enable system language

and fix Ukrainian name

closes #2608 #2278 #346
pull/2575/head
Mikael Finstad 8 months ago
parent 251110b375
commit 2511101a58
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -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',

@ -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,

@ -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<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';
@ -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`);

@ -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]);

@ -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

@ -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<ChangeEventHandler<HTMLSelectElement>>((e) => {
const { value } = e.target;
const l = value !== '' ? value : undefined;
setLanguage(l as LanguageKey | undefined);
setLanguage(l as SupportedLanguage | undefined);
}, [setLanguage]);
const timecodeFormatOptions = useMemo<Record<TimecodeFormat, string>>(() => ({

@ -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,

@ -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<Record<ElectronLanguageKey, string>>;
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,

Loading…
Cancel
Save