feat: i18n support

This commit is contained in:
Xmader 2020-11-05 00:22:15 -05:00
parent d912e7cad9
commit fad4b75dde
4 changed files with 103 additions and 27 deletions

View file

@ -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
View 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
View 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]
}

View file

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