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 i18n from './i18n'
|
||||
|
||||
type BtnElement = HTMLButtonElement
|
||||
|
||||
|
@ -93,13 +94,6 @@ type BtnAction = (btnName: string, btnEl: BtnElement, setText: (str: string) =>
|
|||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
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 UrlInput = Promisable<string> | (() => Promisable<string>)
|
||||
|
||||
|
@ -127,10 +121,10 @@ export namespace BtnAction {
|
|||
return async (btnName, btn, setText) => {
|
||||
const _onclick = btn.onclick
|
||||
btn.onclick = null
|
||||
setText(BtnAction.PROCESSING_TEXT)
|
||||
setText(i18n('PROCESSING')())
|
||||
|
||||
const w = window.open('') as Window
|
||||
const txt = document.createTextNode(BtnAction.PROCESSING_TEXT)
|
||||
const txt = document.createTextNode(i18n('PROCESSING')())
|
||||
w.document.body.append(txt)
|
||||
|
||||
// set page hooks
|
||||
|
@ -159,13 +153,13 @@ export namespace BtnAction {
|
|||
const _onclick = btn.onclick
|
||||
|
||||
btn.onclick = null
|
||||
setText(PROCESSING_TEXT)
|
||||
setText(i18n('PROCESSING')())
|
||||
|
||||
try {
|
||||
await fn()
|
||||
setText(name)
|
||||
} catch (err) {
|
||||
setText(ERROR_TEXT)
|
||||
setText(i18n('BTN_ERROR')())
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
|
@ -175,7 +169,7 @@ export namespace BtnAction {
|
|||
|
||||
export const deprecate = (action: BtnAction): BtnAction => {
|
||||
return (name, btn, setText) => {
|
||||
alert(deprecationNotice(name))
|
||||
alert(i18n('DEPRECATION_NOTICE')(name))
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
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 * as recaptcha from './recaptcha'
|
||||
import scoreinfo from './scoreinfo'
|
||||
import i18n from './i18n'
|
||||
|
||||
const main = (): void => {
|
||||
// init recaptcha
|
||||
|
@ -18,19 +19,19 @@ const main = (): void => {
|
|||
const filename = scoreinfo.fileName
|
||||
|
||||
btnList.add({
|
||||
name: 'Download MSCZ',
|
||||
name: i18n('DOWNLOAD')('MSCZ'),
|
||||
action: BtnAction.process(downloadMscz),
|
||||
})
|
||||
|
||||
btnList.add({
|
||||
name: 'Download PDF',
|
||||
name: i18n('DOWNLOAD')('PDF'),
|
||||
action: BtnAction.deprecate(
|
||||
BtnAction.process(downloadPDF),
|
||||
),
|
||||
})
|
||||
|
||||
btnList.add({
|
||||
name: 'Download MusicXML',
|
||||
name: i18n('DOWNLOAD')('MusicXML'),
|
||||
action: BtnAction.mscoreWindow(async (w, score) => {
|
||||
const mxl = await score.saveMxl()
|
||||
const data = new Blob([mxl])
|
||||
|
@ -40,26 +41,26 @@ const main = (): void => {
|
|||
})
|
||||
|
||||
btnList.add({
|
||||
name: 'Download MIDI',
|
||||
name: i18n('DOWNLOAD')('MIDI'),
|
||||
action: BtnAction.deprecate(
|
||||
BtnAction.download(() => getFileUrl('midi')),
|
||||
),
|
||||
})
|
||||
|
||||
btnList.add({
|
||||
name: 'Download MP3',
|
||||
name: i18n('DOWNLOAD')('MP3'),
|
||||
action: BtnAction.download(() => getFileUrl('mp3')),
|
||||
})
|
||||
|
||||
btnList.add({
|
||||
name: 'Individual Parts',
|
||||
tooltip: 'Download individual parts (BETA)',
|
||||
name: i18n('IND_PARTS')(),
|
||||
tooltip: i18n('IND_PARTS_TOOLTIP')(),
|
||||
action: BtnAction.mscoreWindow(async (w, score, txt) => {
|
||||
const metadata = await score.metadata()
|
||||
console.log('score metadata loaded by webmscore', metadata)
|
||||
|
||||
// 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
|
||||
txt.remove()
|
||||
|
@ -74,32 +75,32 @@ const main = (): void => {
|
|||
|
||||
const downloads: IndividualDownload[] = [
|
||||
{
|
||||
name: 'Download PDF',
|
||||
name: i18n('DOWNLOAD')('PDF'),
|
||||
fileExt: 'pdf',
|
||||
action: (score) => score.savePdf(),
|
||||
},
|
||||
{
|
||||
name: 'Download Part MSCZ',
|
||||
name: i18n('DOWNLOAD')('MSCZ'),
|
||||
fileExt: 'mscz',
|
||||
action: (score) => score.saveMsc('mscz'),
|
||||
},
|
||||
{
|
||||
name: 'Download Part MusicXML',
|
||||
name: i18n('DOWNLOAD')('MusicXML'),
|
||||
fileExt: 'mxl',
|
||||
action: (score) => score.saveMxl(),
|
||||
},
|
||||
{
|
||||
name: 'Download MIDI',
|
||||
name: i18n('DOWNLOAD')('MIDI'),
|
||||
fileExt: 'mid',
|
||||
action: (score) => score.saveMidi(true, true),
|
||||
},
|
||||
{
|
||||
name: 'Download FLAC Audio',
|
||||
name: i18n('DOWNLOAD_AUDIO')('FLAC'),
|
||||
fileExt: 'flac',
|
||||
action: (score) => loadSoundFont(score).then(() => score.saveAudio('flac')),
|
||||
},
|
||||
{
|
||||
name: 'Download OGG Audio',
|
||||
name: i18n('DOWNLOAD_AUDIO')('OGG'),
|
||||
fileExt: 'ogg',
|
||||
action: (score) => loadSoundFont(score).then(() => score.saveAudio('ogg')),
|
||||
},
|
||||
|
@ -145,7 +146,7 @@ const main = (): void => {
|
|||
// lock the button when processing
|
||||
submitBtn.onclick = null
|
||||
submitBtn.disabled = true
|
||||
submitBtn.value = 'Processing…'
|
||||
submitBtn.value = i18n('PROCESSING')()
|
||||
|
||||
const checked = fieldset.querySelector('input:checked') as HTMLInputElement
|
||||
const partName = checked.alt
|
||||
|
|
Loading…
Reference in a new issue