feat: i18n support
This commit is contained in:
parent
d912e7cad9
commit
fad4b75dde
4 changed files with 103 additions and 27 deletions
18
src/btn.ts
18
src/btn.ts
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
import { loadMscore, WebMscore } from './mscore'
|
import { loadMscore, WebMscore } from './mscore'
|
||||||
|
import i18n from './i18n'
|
||||||
|
|
||||||
type BtnElement = HTMLButtonElement
|
type BtnElement = HTMLButtonElement
|
||||||
|
|
||||||
|
@ -93,13 +94,6 @@ type BtnAction = (btnName: string, btnEl: BtnElement, setText: (str: string) =>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
export namespace BtnAction {
|
export namespace BtnAction {
|
||||||
|
|
||||||
export const PROCESSING_TEXT = 'Processing…'
|
|
||||||
export const ERROR_TEXT = '❌Download Failed!'
|
|
||||||
|
|
||||||
const deprecationNotice = (btnName: string): string => {
|
|
||||||
return `DEPRECATED!\nUse \`${btnName}\` inside \`Individual Parts\` instead.\n(This may still work. Click \`OK\` to continue.)`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Promisable<T> = T | Promise<T>
|
type Promisable<T> = T | Promise<T>
|
||||||
type UrlInput = Promisable<string> | (() => Promisable<string>)
|
type UrlInput = Promisable<string> | (() => Promisable<string>)
|
||||||
|
|
||||||
|
@ -127,10 +121,10 @@ export namespace BtnAction {
|
||||||
return async (btnName, btn, setText) => {
|
return async (btnName, btn, setText) => {
|
||||||
const _onclick = btn.onclick
|
const _onclick = btn.onclick
|
||||||
btn.onclick = null
|
btn.onclick = null
|
||||||
setText(BtnAction.PROCESSING_TEXT)
|
setText(i18n('PROCESSING')())
|
||||||
|
|
||||||
const w = window.open('') as Window
|
const w = window.open('') as Window
|
||||||
const txt = document.createTextNode(BtnAction.PROCESSING_TEXT)
|
const txt = document.createTextNode(i18n('PROCESSING')())
|
||||||
w.document.body.append(txt)
|
w.document.body.append(txt)
|
||||||
|
|
||||||
// set page hooks
|
// set page hooks
|
||||||
|
@ -159,13 +153,13 @@ export namespace BtnAction {
|
||||||
const _onclick = btn.onclick
|
const _onclick = btn.onclick
|
||||||
|
|
||||||
btn.onclick = null
|
btn.onclick = null
|
||||||
setText(PROCESSING_TEXT)
|
setText(i18n('PROCESSING')())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fn()
|
await fn()
|
||||||
setText(name)
|
setText(name)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setText(ERROR_TEXT)
|
setText(i18n('BTN_ERROR')())
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,7 +169,7 @@ export namespace BtnAction {
|
||||||
|
|
||||||
export const deprecate = (action: BtnAction): BtnAction => {
|
export const deprecate = (action: BtnAction): BtnAction => {
|
||||||
return (name, btn, setText) => {
|
return (name, btn, setText) => {
|
||||||
alert(deprecationNotice(name))
|
alert(i18n('DEPRECATION_NOTICE')(name))
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return action(name, btn, setText)
|
return action(name, btn, setText)
|
||||||
}
|
}
|
||||||
|
|
33
src/i18n/en.ts
Normal file
33
src/i18n/en.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
|
||||||
|
import { createLocale } from './'
|
||||||
|
|
||||||
|
export default createLocale({
|
||||||
|
'PROCESSING' () {
|
||||||
|
return 'Processing…' as const
|
||||||
|
},
|
||||||
|
'BTN_ERROR' () {
|
||||||
|
return '❌Download Failed!' as const
|
||||||
|
},
|
||||||
|
|
||||||
|
'DEPRECATION_NOTICE' (btnName: string) {
|
||||||
|
return `DEPRECATED!\nUse \`${btnName}\` inside \`Individual Parts\` instead.\n(This may still work. Click \`OK\` to continue.)` as const
|
||||||
|
},
|
||||||
|
|
||||||
|
'DOWNLOAD' <T extends string> (fileType: T) {
|
||||||
|
return `Download ${fileType}` as const
|
||||||
|
},
|
||||||
|
'DOWNLOAD_AUDIO' <T extends string> (fileType: T) {
|
||||||
|
return `Download ${fileType} Audio` as const
|
||||||
|
},
|
||||||
|
|
||||||
|
'IND_PARTS' () {
|
||||||
|
return 'Individual Parts' as const
|
||||||
|
},
|
||||||
|
'IND_PARTS_TOOLTIP' () {
|
||||||
|
return 'Download individual parts (BETA)' as const
|
||||||
|
},
|
||||||
|
|
||||||
|
'FULL_SCORE' () {
|
||||||
|
return 'Full score' as const
|
||||||
|
},
|
||||||
|
})
|
48
src/i18n/index.ts
Normal file
48
src/i18n/index.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
|
||||||
|
import en from './en'
|
||||||
|
|
||||||
|
export interface LOCALE {
|
||||||
|
'PROCESSING' (): string;
|
||||||
|
'BTN_ERROR' (): string;
|
||||||
|
|
||||||
|
'DEPRECATION_NOTICE' (btnName: string): string;
|
||||||
|
|
||||||
|
'DOWNLOAD' (fileType: string): string;
|
||||||
|
'DOWNLOAD_AUDIO' (fileType: string): string;
|
||||||
|
|
||||||
|
'IND_PARTS' (): string;
|
||||||
|
'IND_PARTS_TOOLTIP' (): string;
|
||||||
|
|
||||||
|
'FULL_SCORE' (): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* type checking only so no missing keys
|
||||||
|
*/
|
||||||
|
export function createLocale<OBJ extends LOCALE> (obj: OBJ): OBJ {
|
||||||
|
return Object.freeze(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
const locales = (<L extends { [n: string]: LOCALE } /** type checking */> (l: L) => Object.freeze(l))({
|
||||||
|
en,
|
||||||
|
})
|
||||||
|
|
||||||
|
// detect browser language
|
||||||
|
const lang = (() => {
|
||||||
|
const names = Object.keys(locales)
|
||||||
|
const _lang = navigator.languages.find(l => {
|
||||||
|
// find the first occurrence of valid languages
|
||||||
|
return names.includes(l)
|
||||||
|
})
|
||||||
|
return _lang || 'en'
|
||||||
|
})()
|
||||||
|
|
||||||
|
export type STR_KEYS = keyof LOCALE
|
||||||
|
export type ALL_LOCALES = typeof locales
|
||||||
|
export type LANGS = keyof ALL_LOCALES
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
export default function i18n<K extends STR_KEYS, L extends LANGS = 'en'> (key: K) {
|
||||||
|
const locale = locales[lang] as ALL_LOCALES[L]
|
||||||
|
return locale[key]
|
||||||
|
}
|
31
src/main.ts
31
src/main.ts
|
@ -8,6 +8,7 @@ import { WebMscore, loadSoundFont } from './mscore'
|
||||||
import { getDownloadBtn, BtnList, BtnAction } from './btn'
|
import { getDownloadBtn, BtnList, BtnAction } from './btn'
|
||||||
import * as recaptcha from './recaptcha'
|
import * as recaptcha from './recaptcha'
|
||||||
import scoreinfo from './scoreinfo'
|
import scoreinfo from './scoreinfo'
|
||||||
|
import i18n from './i18n'
|
||||||
|
|
||||||
const main = (): void => {
|
const main = (): void => {
|
||||||
// init recaptcha
|
// init recaptcha
|
||||||
|
@ -18,19 +19,19 @@ const main = (): void => {
|
||||||
const filename = scoreinfo.fileName
|
const filename = scoreinfo.fileName
|
||||||
|
|
||||||
btnList.add({
|
btnList.add({
|
||||||
name: 'Download MSCZ',
|
name: i18n('DOWNLOAD')('MSCZ'),
|
||||||
action: BtnAction.process(downloadMscz),
|
action: BtnAction.process(downloadMscz),
|
||||||
})
|
})
|
||||||
|
|
||||||
btnList.add({
|
btnList.add({
|
||||||
name: 'Download PDF',
|
name: i18n('DOWNLOAD')('PDF'),
|
||||||
action: BtnAction.deprecate(
|
action: BtnAction.deprecate(
|
||||||
BtnAction.process(downloadPDF),
|
BtnAction.process(downloadPDF),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
btnList.add({
|
btnList.add({
|
||||||
name: 'Download MusicXML',
|
name: i18n('DOWNLOAD')('MusicXML'),
|
||||||
action: BtnAction.mscoreWindow(async (w, score) => {
|
action: BtnAction.mscoreWindow(async (w, score) => {
|
||||||
const mxl = await score.saveMxl()
|
const mxl = await score.saveMxl()
|
||||||
const data = new Blob([mxl])
|
const data = new Blob([mxl])
|
||||||
|
@ -40,26 +41,26 @@ const main = (): void => {
|
||||||
})
|
})
|
||||||
|
|
||||||
btnList.add({
|
btnList.add({
|
||||||
name: 'Download MIDI',
|
name: i18n('DOWNLOAD')('MIDI'),
|
||||||
action: BtnAction.deprecate(
|
action: BtnAction.deprecate(
|
||||||
BtnAction.download(() => getFileUrl('midi')),
|
BtnAction.download(() => getFileUrl('midi')),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
btnList.add({
|
btnList.add({
|
||||||
name: 'Download MP3',
|
name: i18n('DOWNLOAD')('MP3'),
|
||||||
action: BtnAction.download(() => getFileUrl('mp3')),
|
action: BtnAction.download(() => getFileUrl('mp3')),
|
||||||
})
|
})
|
||||||
|
|
||||||
btnList.add({
|
btnList.add({
|
||||||
name: 'Individual Parts',
|
name: i18n('IND_PARTS')(),
|
||||||
tooltip: 'Download individual parts (BETA)',
|
tooltip: i18n('IND_PARTS_TOOLTIP')(),
|
||||||
action: BtnAction.mscoreWindow(async (w, score, txt) => {
|
action: BtnAction.mscoreWindow(async (w, score, txt) => {
|
||||||
const metadata = await score.metadata()
|
const metadata = await score.metadata()
|
||||||
console.log('score metadata loaded by webmscore', metadata)
|
console.log('score metadata loaded by webmscore', metadata)
|
||||||
|
|
||||||
// add the "full score" option as a "part"
|
// add the "full score" option as a "part"
|
||||||
metadata.excerpts.unshift({ id: -1, title: 'Full score', parts: [] })
|
metadata.excerpts.unshift({ id: -1, title: i18n('FULL_SCORE')(), parts: [] })
|
||||||
|
|
||||||
// render the part selection page
|
// render the part selection page
|
||||||
txt.remove()
|
txt.remove()
|
||||||
|
@ -74,32 +75,32 @@ const main = (): void => {
|
||||||
|
|
||||||
const downloads: IndividualDownload[] = [
|
const downloads: IndividualDownload[] = [
|
||||||
{
|
{
|
||||||
name: 'Download PDF',
|
name: i18n('DOWNLOAD')('PDF'),
|
||||||
fileExt: 'pdf',
|
fileExt: 'pdf',
|
||||||
action: (score) => score.savePdf(),
|
action: (score) => score.savePdf(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Download Part MSCZ',
|
name: i18n('DOWNLOAD')('MSCZ'),
|
||||||
fileExt: 'mscz',
|
fileExt: 'mscz',
|
||||||
action: (score) => score.saveMsc('mscz'),
|
action: (score) => score.saveMsc('mscz'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Download Part MusicXML',
|
name: i18n('DOWNLOAD')('MusicXML'),
|
||||||
fileExt: 'mxl',
|
fileExt: 'mxl',
|
||||||
action: (score) => score.saveMxl(),
|
action: (score) => score.saveMxl(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Download MIDI',
|
name: i18n('DOWNLOAD')('MIDI'),
|
||||||
fileExt: 'mid',
|
fileExt: 'mid',
|
||||||
action: (score) => score.saveMidi(true, true),
|
action: (score) => score.saveMidi(true, true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Download FLAC Audio',
|
name: i18n('DOWNLOAD_AUDIO')('FLAC'),
|
||||||
fileExt: 'flac',
|
fileExt: 'flac',
|
||||||
action: (score) => loadSoundFont(score).then(() => score.saveAudio('flac')),
|
action: (score) => loadSoundFont(score).then(() => score.saveAudio('flac')),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Download OGG Audio',
|
name: i18n('DOWNLOAD_AUDIO')('OGG'),
|
||||||
fileExt: 'ogg',
|
fileExt: 'ogg',
|
||||||
action: (score) => loadSoundFont(score).then(() => score.saveAudio('ogg')),
|
action: (score) => loadSoundFont(score).then(() => score.saveAudio('ogg')),
|
||||||
},
|
},
|
||||||
|
@ -145,7 +146,7 @@ const main = (): void => {
|
||||||
// lock the button when processing
|
// lock the button when processing
|
||||||
submitBtn.onclick = null
|
submitBtn.onclick = null
|
||||||
submitBtn.disabled = true
|
submitBtn.disabled = true
|
||||||
submitBtn.value = 'Processing…'
|
submitBtn.value = i18n('PROCESSING')()
|
||||||
|
|
||||||
const checked = fieldset.querySelector('input:checked') as HTMLInputElement
|
const checked = fieldset.querySelector('input:checked') as HTMLInputElement
|
||||||
const partName = checked.alt
|
const partName = checked.alt
|
||||||
|
|
Loading…
Reference in a new issue