Skip to content

Commit

Permalink
✨ feat: Multi-Language (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
lingbopro committed Jun 22, 2024
1 parent b38e3be commit d062999
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 55 deletions.
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"topbar"
]
}
7 changes: 5 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<!-- Use [Prettier](https://prettier.io) to format your code. -->
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.
Expand Down Expand Up @@ -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.
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.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down Expand Up @@ -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.
153 changes: 153 additions & 0 deletions assets/js/lang.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* 存储语言文本
* @type {Object<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<String>}
*/
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');
}
24 changes: 12 additions & 12 deletions assets/js/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
Expand All @@ -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 = '';
Expand Down Expand Up @@ -103,11 +99,15 @@ function updateCurrentMatch() {
match.innerHTML = `
<div class="player">
<label>${player1}: <span>${currentMatchScores[player1]}</span></label>
<s-button type="filled-tonal" class="score-button" onclick="incrementCurrentMatchScore('${player1}')">Score</s-button>
<s-button type="filled-tonal" class="score-button" onclick="incrementCurrentMatchScore('${player1}')">
${lang('ui.gameBoard.score')}
</s-button>
</div>
<div class="player">
<label>${player2}: <span>${currentMatchScores[player2]}</span></label>
<s-button type="filled-tonal" class="score-button" onclick="incrementCurrentMatchScore('${player2}')">Score</s-button>
<s-button type="filled-tonal" class="score-button" onclick="incrementCurrentMatchScore('${player2}')">
${lang('ui.gameBoard.score')}
</s-button>
</div>
`;
}
Expand All @@ -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);
}
}
Expand Down
35 changes: 20 additions & 15 deletions assets/js/ui.js
Original file line number Diff line number Diff line change
@@ -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 += `
<s-menu-item onclick="setLanguage('${currentValue}')">
${langForce(currentValue, 'language.LanguageName')}
</s-menu-item>`;
})
updateElementLanguages();
});

function showSnackBar(message, id = 'snackbar') {
Expand Down Expand Up @@ -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"></path>
</svg>`;
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');
}
59 changes: 59 additions & 0 deletions assets/js/utils.js
Original file line number Diff line number Diff line change
@@ -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));
}
Loading

0 comments on commit d062999

Please sign in to comment.