From d06299973bb54f917377ea82348f65475353df09 Mon Sep 17 00:00:00 2001 From: Lingbo Date: Sat, 22 Jun 2024 11:25:24 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Multi-Language=20(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 5 ++ CONTRIBUTING.md | 7 +- README.md | 3 +- assets/js/lang.js | 153 +++++++++++++++++++++++++++++++++++++ assets/js/script.js | 24 +++--- assets/js/ui.js | 35 +++++---- assets/js/utils.js | 59 ++++++++++++++ docs/CONTRIBUTING.zh-CN.md | 9 ++- index.html | 66 +++++++++++----- 9 files changed, 306 insertions(+), 55 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 assets/js/lang.js create mode 100644 assets/js/utils.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..afaaec0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "topbar" + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d196fdb..98aa1ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,6 @@ Welcome your contributions! Please follow these guidelines: Fork the repository. Create a new branch for your feature or bug fix. Write your code for the changes. - Commit your changes with a descriptive message. **Note: The commit message _must_ follow the [format below](#commit-message-format)** Push your branch to your fork. @@ -53,4 +52,8 @@ The subject contains a brief description of the change. #### footer Footer. -The footer should contain any information about major changes and is also the location to reference the GitHub Issue that this commit closes. \ No newline at end of file +The footer should contain any information about major changes and is also the location to reference the GitHub Issue that this commit closes. + +## Hint +- Use [Prettier](https://prettier.io) to format your code. +- Never use hard-coded values. Use language file ([assets/js/lang.js](./assets/js/lang.js)) instead. \ No newline at end of file diff --git a/README.md b/README.md index 4d23ce3..6a533e7 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ The Pool Score Tracker is a web application that allows you to track scores for + Show match order + Maintain match history + Responsive design with a user-friendly interface ++ Multi Language Support ### Usage + **Set Winning Balls:** @@ -38,4 +39,4 @@ The Pool Score Tracker is a web application that allows you to track scores for [Contribution Guide [English]](./CONTRIBUTING.md) | [贡献指南 [中文]](./docs/CONTRIBUTING.zh-CN.md) ## License -This project is licensed under the Apache-2.0 License. See the LICENSE file for details. +This project is licensed under the Apache-2.0 License. See the [LICENSE](./LICENSE) file for details. diff --git a/assets/js/lang.js b/assets/js/lang.js new file mode 100644 index 0000000..76a98c1 --- /dev/null +++ b/assets/js/lang.js @@ -0,0 +1,153 @@ +/** + * 存储语言文本 + * @type {Object} + */ +const languages = { + 'en-US': { + 'language.LanguageName': 'English', + 'ui.title': 'Table Tennis Counter', + 'ui.theme.themeName.auto': 'auto', + 'ui.theme.themeName.light': 'light', + 'ui.theme.themeName.dark': 'dark', + 'ui.topbar.language': 'Languages', + 'ui.topbar.theme': 'Theme', + 'ui.topbar.github': 'GitHub', + 'ui.setup.winningBalls': 'Set Winning Balls', + 'ui.setup.playerName': 'Player Name', + 'ui.setup.addPlayer': 'Add Player', + 'ui.setup.playerListHint': 'Players:', + 'ui.setup.startGame': 'Start Game', + 'ui.gameBoard.playerScores': 'Player Scores', + 'ui.gameBoard.matchHistory': 'Match History', + 'ui.gameBoard.currentMatch': 'Current Match', + 'ui.gameBoard.matchOrder': 'Match Order', + 'ui.gameBoard.score': 'Score', + 'ui.gameBoard.winMessage': '{0} Wins this round!', + 'ui.gameBoard.matchHistory.item': '{0} vs {1}: {2} won', + 'ui.gameBoard.matchOrder.item': '{0} vs {1}', + 'ui.tooltip.themeSetTo': 'Theme set to {0}', + 'ui.tooltip.playerNameError': 'Please enter a unique valid player name', + 'ui.tooltip.playerAmountError': 'Please add at least two players', + 'ui.tooltip.winningBallsError': 'Please enter a valid number of winning balls', + 'ui.tooltip.repoTip': 'Issues and PRs are welcome!!!', + }, +}; +/** + * 当前语言 + * @type {String} + */ +var currentLanguage = 'en-US'; // 这里得用var定义全局变量 + + + +/** + * 这里是逻辑部分 + * 不要乱动,说不定啥时候就炸了( + * + * 不要乱动啊啊啊啊啊 + */ + +/** + * 获取语言列表,返回语言代码的数组 + * @returns {Array} + */ +function getLanguageList() { + let list = []; + for (let lang in languages) { + list.push(lang); + } + return list; +} + +/** + * 从语言文件中取回当前语言 `key` 对应的值,并进行格式化(主动更新) + * 如果当前语言没有此键,则返回英语的内容 + * 如果英语也没有,则返回键名称 + * @param {String} key 键名称 + * @param {String} args 格式化的参数 + * @returns {String} 取回并格式化后的字符串 + */ +function lang(key, ...args) { + return langForce(currentLanguage, key, ...args); +} + +/** + * 从指定的语言文件中取回 `key` 对应的值,并进行格式化(主动更新) + * 如果指定语言没有此键,则返回英语的内容 + * 如果英语也没有,则返回键名称 + * @param {String} lang 语言代码 + * @param {String} key 键名称 + * @param {String} args 格式化的参数 + * @returns {String} 取回并格式化后的字符串 + */ +function langForce(lang, key, ...args) { + if ( + typeof languages[lang] != 'string' || + typeof languages[lang][key] != 'string' + ) { + lang = 'en-US'; + } + if (lang === 'en-US' && typeof languages[lang][key] != 'string') { + return key; + } + return languages[lang][key].format(...args); +} + +/** + * 设置为某语言,并重新加载页面 + * @param {String} language 语言名称 + */ +function setLanguage(language) { + console.log(`Language set to ${language}`); + writeConfig('language', language); + window.location.reload(); +} + +/** + * 更新元素的语言(被动更新) + * 根据元素的 `data-lang` 属性匹配键,并将属性 `data-lang-prop` 对应的属性名设置为此值 + * **此函数执行较慢,非必要不随意调用!** + */ +function updateElementLanguages() { + // 这个函数费了我至少一个半小时...改bug太麻烦了 + console.log('Update Element Languages Start'); + console.time('Update Element Languages'); // 耗时操作,计个时不过分吧 + let elements = document.getElementsByTagName('*'); + for (let i = 0; i < elements.length; i++) { // 遍历每个元素 + let element = elements[i]; + if (element.dataset?.lang) { + // 根据data-lang属性来匹配值 + if ( + element.dataset.langProp || // 如果指定了data-lang-prop则使用此属性 + element.childNodes.length === 0 || // 或者没有子节点(内容是空的) + (element.childNodes.length === 1 && // 或者如果只有一个子节点(只包含文本) + element.childNodes[0].nodeName === '#text') + ) { + let property = element.dataset.langProp ?? 'innerText'; + element[property] = lang(element.dataset.lang); + console.debug( + `Update Element Languages: key '${element.dataset.lang}' property '${property}' %o`, + element + ); + } else { + // 如果元素子节点不止一个(除了文本还包含其他元素) + // 则遍历所有子节点,找到文本节点并设置其值 + // 为了防止影子根被误认成文本必须倒序遍历 + // 血泪的教训啊 + for (let j = element.childNodes.length - 1; j >= 0; j--) { + if (element.childNodes[j].nodeName === '#text') { + element.childNodes[j].nodeValue = lang( + element.dataset.lang + ); + console.debug( + `Update Element Languages: key '${element.dataset.lang}' %o`, + element + ); + break; + } + } + } + } + } + console.timeEnd('Update Element Languages'); +} diff --git a/assets/js/script.js b/assets/js/script.js index 397cb37..85ea23c 100644 --- a/assets/js/script.js +++ b/assets/js/script.js @@ -5,14 +5,10 @@ let currentMatch = [0, 1]; // Indexes of the players in the current match let winBalls = 5; let matchHistory = []; -document.addEventListener('DOMContentLoaded', () => { - // 虽然但是,onclick:? -}); - function addPlayer() { const playerName = document.getElementById('playerName').value.trim(); if (playerName === '' || players.includes(playerName)) { - showSnackBar('Please enter a unique valid player name', 'PlayerNameError'); + showSnackBar(lang('ui.tooltip.playerNameError'), 'PlayerNameError'); return; } players.push(playerName); @@ -36,13 +32,13 @@ function updatePlayerList() { function startGame() { showLoading(); if (players.length < 2) { - showSnackBar('Please add at least two players', 'PlayerAmountError'); + showSnackBar(lang('ui.tooltip.playerAmountError'), 'PlayerAmountError'); hideLoading(); return; } winBalls = parseInt(document.getElementById('winBalls').value); if (isNaN(winBalls) || winBalls <= 0) { - showSnackBar('Please enter a valid number of winning balls', 'WinningBallsError'); + showSnackBar(lang('ui.tooltip.winningBallsError'), 'WinningBallsError'); hideLoading(); return; } @@ -69,8 +65,8 @@ function incrementCurrentMatchScore(playerName) { updateCurrentMatch(); if (currentMatchScores[playerName] >= winBalls) { totalScores[playerName]++; - document.getElementById('result').innerText = `${playerName} Wins this round!`; - matchHistory.push(`${players[currentMatch[0]]} vs ${players[currentMatch[1]]}: ${playerName} won`); + document.getElementById('result').innerText = lang('ui.gameBoard.winMessage', playerName); + matchHistory.push(lang('ui.gameBoard.matchHistory.item', players[currentMatch[0]], players[currentMatch[1]], playerName)); disableScoreButtons(); setTimeout(() => { document.getElementById('result').innerText = ''; @@ -103,11 +99,15 @@ function updateCurrentMatch() { match.innerHTML = `
- Score + + ${lang('ui.gameBoard.score')} +
- Score + + ${lang('ui.gameBoard.score')} +
`; } @@ -129,7 +129,7 @@ function updateMatchOrderList() { const player1 = players[i]; const player2 = players[(i + 1) % players.length]; const li = document.createElement('li'); - li.textContent = `${player1} vs ${player2}`; + li.textContent = lang('ui.gameBoard.matchOrder.item', player1, player2); matchOrderList.appendChild(li); } } diff --git a/assets/js/ui.js b/assets/js/ui.js index b663da5..e0b40b4 100644 --- a/assets/js/ui.js +++ b/assets/js/ui.js @@ -1,15 +1,21 @@ document.addEventListener('DOMContentLoaded', () => { // 虽然但是,onclick:? - let pageEl = document.getElementById('page'); - - document.getElementById('sober').addEventListener('load', function () { - e_toggleTheme('auto') - setTimeout(function () { // 等Sober执行完 - document.getElementById('main').style.visibility = 'visible'; - document.getElementById('top-bar').style.visibility = 'visible'; - hideLoading(); - }, 500); - }); + e_toggleTheme(readConfig('theme', 'auto')); + setTimeout(function () { // 等Sober执行完 + document.getElementById('main').style.visibility = 'visible'; + document.getElementById('top-bar').style.visibility = 'visible'; + hideLoading(); + }, 500); + currentLanguage = readConfig('language', 'en-US'); + let langList = getLanguageList(); + let languageMenuEl = document.getElementById('language-menu'); + langList.forEach(function(currentValue) { + languageMenuEl.innerHTML += ` + + ${langForce(currentValue, 'language.LanguageName')} +`; + }) + updateElementLanguages(); }); function showSnackBar(message, id = 'snackbar') { @@ -59,23 +65,22 @@ function e_toggleTheme(theme) { 134v186H614L480-28Zm0-112 100-100h140v-140l100-100-100-100v-140H580L480-820 380-720H240v140L140-480l100 100v140h140l100 100Zm0-340Z"> `; - showSnackBar('Theme set to auto', 'Theme'); break; case 'light': pageEl.theme = 'light'; themeIconEl.type = 'light_mode'; themeIconEl.innerHTML = ''; - showSnackBar('Theme set to light', 'Theme'); break; case 'dark': pageEl.theme = 'dark'; themeIconEl.type = 'dark_mode'; themeIconEl.innerHTML = ''; - showSnackBar('Theme set to dark', 'Theme'); break; - } + } + writeConfig('theme', theme); + showSnackBar(lang('ui.tooltip.themeSetTo', lang(`ui.theme.themeName.${theme}`)), 'Theme'); } function e_gotoGitHub() { - showSnackBar('Issues and PRs are welcome!!!', 'RepoTips'); + showSnackBar(lang('ui.tooltip.repoTip'), 'RepoTips'); window.open('https://github.com/Minemetero/Table-Tennis-Counter', '_blank'); } \ No newline at end of file diff --git a/assets/js/utils.js b/assets/js/utils.js new file mode 100644 index 0000000..6d2c018 --- /dev/null +++ b/assets/js/utils.js @@ -0,0 +1,59 @@ +// 万恶的util... + +// 字符串格式化 +// 绝对不是从 https://www.cnblogs.com/soymilk2019/p/15388984.html 抄的( +if (!String.prototype.format) { + String.prototype.format = function () { + var args = arguments; + return this.replace(/{(\d+)}/g, function (match, number) { + return typeof args[number] != 'undefined' ? args[number] : match; + }); + }; +} +/** + * 从localStorage读取配置项,找不到返回defaultValue + * @param {String} key 项名 + * @param {*} [defaultValue=null] 找不到时返回的默认值 + * @returns {*} 获取到的配置项 + */ +function readConfig(key, defaultValue = null) { + try { + let configString = localStorage.getItem('Table-Tennis-Counter'); + if (typeof configString == 'string' && configString !== null) { + let config = JSON.parse(configString); + if (typeof config == 'object' && typeof config[key] != 'undefined' && config[key] !== null) { + return config[key]; + } else { + return defaultValue; + } + } else { + return defaultValue; + } + } catch (error) { + return defaultValue; + } +} + +/** + * 将配置项写入到localStorage + * @param {String} key 项名 + * @param {*} value 要写入的内容 + */ +function writeConfig(key, value) { + let configString = localStorage.getItem('Table-Tennis-Counter'); + let config = {}; + if (typeof configString == 'string' && config !== null) { + try { + config = JSON.parse(configString); + if (!(typeof config == 'object' && config !== null)) { + config = {}; + } + } catch (error) { + config = {}; + } + } else { + config = {}; + } + config[key] = value; + localStorage.setItem('Table-Tennis-Counter', JSON.stringify(config)); +} diff --git a/docs/CONTRIBUTING.zh-CN.md b/docs/CONTRIBUTING.zh-CN.md index e6f10fe..2fbe7fd 100644 --- a/docs/CONTRIBUTING.zh-CN.md +++ b/docs/CONTRIBUTING.zh-CN.md @@ -7,9 +7,6 @@ Fork 存储库。 为您的功能或 bug 修复创建一个新分支。 为您的更改编写代码。 - - - 使用描述性消息提交您的更改。 **注意:更改消息应符合 [下文](#提交消息格式) 的格式** 将您的分支推送到您的 fork。 @@ -56,4 +53,8 @@ Fork 存储库。 #### 页脚 **可选。** 页脚。 -页脚应包含有关重大更改的任何信息,并且也是引用此提交关闭的 GitHub Issue 的位置。 \ No newline at end of file +页脚应包含有关重大更改的任何信息,并且也是引用此提交关闭的 GitHub Issue 的位置。 + +## 提示 +- 使用 [Prettier](https://prettier.cn) 格式化您的代码。 +- 切勿使用硬编码值。请改用语言文件 ([assets/js/lang.js](./../assets/js/lang.js))。 \ No newline at end of file diff --git a/index.html b/index.html index dff57ae..9ae0537 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Table Tennis Counter + Table Tennis Counter @@ -11,16 +11,32 @@ -
Table Tennis Counter
+
Table Tennis Counter
- + + + + + + + + + + + + + + + - Theme - - + + @@ -38,44 +54,50 @@ - GitHub
- + +
- + + - + + - Add Player
-

Players:

+

    - Start Game + + +
    -

    Player Scores

    + +

      -

      Match History

      + +

        -

        Current Match

        + +

        @@ -84,7 +106,8 @@

        Current Match

        -

        Match Order

        + +

          @@ -97,10 +120,11 @@

          Match Order

          - - - + + + + - +