From fad4b75ddeccf2f309506d470692588d1a16fe5c Mon Sep 17 00:00:00 2001 From: Xmader Date: Thu, 5 Nov 2020 00:22:15 -0500 Subject: [PATCH] feat: i18n support --- src/btn.ts | 18 ++++++------------ src/i18n/en.ts | 33 ++++++++++++++++++++++++++++++++ src/i18n/index.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.ts | 31 +++++++++++++++--------------- 4 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 src/i18n/en.ts create mode 100644 src/i18n/index.ts diff --git a/src/btn.ts b/src/btn.ts index 64fb26f..8ece248 100644 --- a/src/btn.ts +++ b/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 | Promise type UrlInput = Promisable | (() => Promisable) @@ -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) } diff --git a/src/i18n/en.ts b/src/i18n/en.ts new file mode 100644 index 0000000..ea1f9ec --- /dev/null +++ b/src/i18n/en.ts @@ -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' (fileType: T) { + return `Download ${fileType}` as const + }, + 'DOWNLOAD_AUDIO' (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 + }, +}) diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..1f1c488 --- /dev/null +++ b/src/i18n/index.ts @@ -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: OBJ): OBJ { + return Object.freeze(obj) +} + +const locales = ( (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 (key: K) { + const locale = locales[lang] as ALL_LOCALES[L] + return locale[key] +} diff --git a/src/main.ts b/src/main.ts index edc3d7c..ef8d951 100644 --- a/src/main.ts +++ b/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