Skip to content

Commit 504a433

Browse files
committed
docs: localize custom game speed generator
1 parent d8a5cab commit 504a433

3 files changed

Lines changed: 113 additions & 72 deletions

File tree

docs/.vitepress/theme/components/CustomGameSpeedGenerator.vue

Lines changed: 109 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div class="custom-game-speed-generator">
33
<label class="custom-game-speed-generator__label" for="custom-game-speed-generator-input">
4-
Enter desired FPS
4+
{{ localizedMessages.desiredFpsLabel }}
55
</label>
66
<input
77
id="custom-game-speed-generator-input"
@@ -12,61 +12,128 @@
1212
step="1"
1313
type="number"
1414
/>
15-
<p class="custom-game-speed-generator__caption">Results (remember to replace N with your game speed number!):</p>
16-
<div class="language-ini vp-adaptive-theme">
17-
<button title="Copy Code" class="copy"></button>
15+
<p class="custom-game-speed-generator__caption">{{ localizedMessages.resultsCaption }}</p>
16+
<div v-if="highlightedCodeHtml" class="language-ini vp-adaptive-theme">
17+
<button :aria-label="localizedMessages.copyCode" :title="localizedMessages.copyCode" class="copy"></button>
1818
<span class="lang">ini</span>
1919
<pre
2020
class="shiki shiki-themes github-light github-dark vp-code"
2121
tabindex="0"
22-
><code v-html="highlightedCodeHtml"></code></pre>
22+
><code v-html="highlightedCodeHtml" />
23+
</pre>
2324
</div>
2425
</div>
2526
</template>
2627

2728
<script setup lang="ts">
2829
import { computed, onBeforeUnmount, ref, watch } from 'vue'
30+
import { useData } from 'vitepress'
2931
import { createHighlighterCore } from 'shiki/core'
3032
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
3133
import ini from '@shikijs/langs/ini'
3234
import githubDark from '@shikijs/themes/github-dark'
3335
import githubLight from '@shikijs/themes/github-light'
3436
37+
const localizedStrings = {
38+
'en-US': {
39+
desiredFpsLabel: 'Enter desired FPS',
40+
resultsCaption: 'Results (remember to replace N with your game speed number!):',
41+
copyCode: 'Copy Code',
42+
noResults: "Sorry, couldn't find anything!",
43+
or: 'Or',
44+
},
45+
'zh-CN': {
46+
desiredFpsLabel: '输入所需的 FPS',
47+
resultsCaption: '结果(别忘了把 N 替换成你的游戏速度编号):',
48+
copyCode: '复制代码',
49+
noResults: '抱歉,未找到任何结果!',
50+
or: '',
51+
},
52+
} as const
53+
54+
const fallbackLocale = 'en-US'
55+
const minGameSpeedDelay = 0
56+
const maxStableGameSpeedDelay = 5
57+
const minChangeInterval = 1
58+
const maxChangeInterval = 40
59+
60+
type LocalizationLocale = keyof typeof localizedStrings
61+
3562
type GameSpeedMatch = {
3663
defaultDelay: number
3764
changeDelay: number
3865
changeInterval: number
3966
}
4067
68+
const { lang } = useData()
4169
const desiredFps = ref<number>(30)
42-
const highlightedCodeHtml = ref<string>('')
43-
let highlightRequestId = 0
4470
4571
const highlighterPromise = createHighlighterCore({
4672
langs: [ini],
4773
themes: [githubLight, githubDark],
4874
engine: createJavaScriptRegexEngine(),
4975
})
5076
51-
function calculateFps(changeDelay: number, defaultDelay: number, changeInterval: number): number {
52-
return (
53-
(60 / (6 - changeDelay) + (60 / (6 - defaultDelay)) * ((changeInterval - 1) / (6 - changeDelay))) /
54-
(1 + (changeInterval - 1) / (6 - changeDelay))
55-
)
56-
}
77+
const localizedMessages = computed(() => {
78+
return localizedStrings[lang.value as LocalizationLocale] || localizedStrings[fallbackLocale]
79+
})
80+
81+
const matchingGameSpeedSettings = computed(() => {
82+
return findMatchingGameSpeedSettings(desiredFps.value)
83+
})
84+
85+
const resultText = computed(() => {
86+
if (!matchingGameSpeedSettings.value.length) {
87+
return `; ${localizedMessages.value.noResults}`
88+
}
5789
58-
const matches = computed<GameSpeedMatch[]>(() => {
59-
const fps = Number(desiredFps.value)
90+
const settings = matchingGameSpeedSettings.value
91+
.map((match, index) => formatGameSpeedMatch(match, index, localizedMessages.value.or))
92+
.join('\n')
93+
94+
return `[General]\nCustomGS=true\n;\n${settings}`
95+
})
96+
const highlightedCodeHtml = ref<String | null>(null)
6097
98+
let highlightRequestId = 0
99+
watch(
100+
resultText,
101+
async (code: string) => {
102+
// Shiki is loaded asynchronously; ignore stale highlights if the user
103+
// changes FPS again before the previous highlight request finishes.
104+
const requestId = (highlightRequestId += 1)
105+
highlightedCodeHtml.value = escapeHtml(code)
106+
107+
const highlighter = await highlighterPromise
108+
const highlightedHtml = highlighter.codeToHtml(code, {
109+
lang: 'ini',
110+
themes: {
111+
light: 'github-light',
112+
dark: 'github-dark',
113+
},
114+
})
115+
116+
if (requestId !== highlightRequestId) {
117+
return
118+
}
119+
120+
highlightedCodeHtml.value = normalizeVitePressShikiTokenStyles(extractCodeHtml(highlightedHtml, code))
121+
},
122+
{ immediate: true },
123+
)
124+
125+
function findMatchingGameSpeedSettings(fps: number): GameSpeedMatch[] {
61126
if (!Number.isFinite(fps)) {
62127
return []
63128
}
64129
65130
const result: GameSpeedMatch[] = []
66131
67-
for (let defaultDelay = 0; defaultDelay <= 5; defaultDelay += 1) {
68-
for (let changeDelay = 0; changeDelay <= 5; changeDelay += 1) {
69-
for (let changeInterval = 1; changeInterval <= 40; changeInterval += 1) {
132+
// The game accepts delay values, not FPS directly. Try every stable
133+
// combination and keep the ones that round to the requested frame rate.
134+
for (let defaultDelay = minGameSpeedDelay; defaultDelay <= maxStableGameSpeedDelay; defaultDelay += 1) {
135+
for (let changeDelay = minGameSpeedDelay; changeDelay <= maxStableGameSpeedDelay; changeDelay += 1) {
136+
for (let changeInterval = minChangeInterval; changeInterval <= maxChangeInterval; changeInterval += 1) {
70137
if (Math.round(calculateFps(changeDelay, defaultDelay, changeInterval)) === fps) {
71138
result.push({ defaultDelay, changeDelay, changeInterval })
72139
}
@@ -75,36 +142,28 @@ const matches = computed<GameSpeedMatch[]>(() => {
75142
}
76143
77144
return result
78-
})
79-
80-
const resultText = computed(() => {
81-
if (!matches.value.length) {
82-
return "// Sorry, couldn't find anything!"
83-
}
145+
}
84146
85-
return matches.value
86-
.map((match, index) => {
87-
const lines = [
88-
`CustomGSN.DefaultDelay=${match.defaultDelay}`,
89-
`CustomGSN.ChangeDelay=${match.changeDelay}`,
90-
`CustomGSN.ChangeInterval=${match.changeInterval}`,
91-
]
147+
function formatGameSpeedMatch(match: GameSpeedMatch, index: number, orLabel: string): string {
148+
const lines = [
149+
`CustomGSN.DefaultDelay=${match.defaultDelay}`,
150+
`CustomGSN.ChangeDelay=${match.changeDelay}`,
151+
`CustomGSN.ChangeInterval=${match.changeInterval}`,
152+
]
92153
93-
if (index > 0) {
94-
lines.unshift('// -- Or --')
95-
}
154+
if (index > 0) {
155+
lines.unshift(`; -- ${orLabel} --`)
156+
}
96157
97-
return lines.join('\n')
98-
})
99-
.join('\n')
100-
})
158+
return lines.join('\n')
159+
}
101160
102161
function escapeHtml(value: string): string {
103162
return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
104163
}
105164
106-
function extractCodeHtml(html: string): string {
107-
return html.match(/<code>([\s\S]*)<\/code>/)?.[1] || escapeHtml(resultText.value)
165+
function extractCodeHtml(html: string, fallbackCode: string): string {
166+
return html.match(/<code>([\s\S]*)<\/code>/)?.[1] || escapeHtml(fallbackCode)
108167
}
109168
110169
function normalizeVitePressShikiTokenStyles(html: string): string {
@@ -126,6 +185,8 @@ function normalizeVitePressShikiTokenStyles(html: string): string {
126185
const property = declaration.slice(0, separatorIndex).trim()
127186
const value = declaration.slice(separatorIndex + 1).trim()
128187
188+
// VitePress themes expect Shiki colors in CSS variables so the same
189+
// generated markup can switch between light and dark mode.
129190
if (property === 'color') {
130191
lightColor = value
131192
continue
@@ -142,31 +203,14 @@ function normalizeVitePressShikiTokenStyles(html: string): string {
142203
})
143204
}
144205
145-
const stopHighlightWatcher = watch(
146-
resultText,
147-
async code => {
148-
const requestId = (highlightRequestId += 1)
149-
highlightedCodeHtml.value = escapeHtml(code)
150-
151-
const highlighter = await highlighterPromise
152-
const highlightedHtml = highlighter.codeToHtml(code, {
153-
lang: 'ini',
154-
themes: {
155-
light: 'github-light',
156-
dark: 'github-dark',
157-
},
158-
})
159-
160-
if (requestId !== highlightRequestId) {
161-
return
162-
}
163-
164-
highlightedCodeHtml.value = normalizeVitePressShikiTokenStyles(extractCodeHtml(highlightedHtml))
165-
},
166-
{ immediate: true },
167-
)
168-
169-
onBeforeUnmount(stopHighlightWatcher)
206+
function calculateFps(changeDelay: number, defaultDelay: number, changeInterval: number): number {
207+
// CustomGS alternates one changed-delay frame with a run of default-delay
208+
// frames. This weighted average estimates the resulting visible FPS.
209+
return (
210+
(60 / (6 - changeDelay) + (60 / (6 - defaultDelay)) * ((changeInterval - 1) / (6 - changeDelay))) /
211+
(1 + (changeInterval - 1) / (6 - changeDelay))
212+
)
213+
}
170214
</script>
171215

172216
<style scoped>

docs/cspell.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@
77
//
88
"./package.json"
99
],
10-
"words": ["vitepress"]
10+
"words": [
11+
"Shiki", //
12+
"vitepress"
13+
]
1114
}

docs/locale/zh_CN/LC_MESSAGES/Miscellanous.po

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,6 @@ msgstr "目前无法直接设置所需的 FPS。需要使用下面的生成器
223223
msgid "Click to show the generator"
224224
msgstr "点击显示生成器"
225225

226-
msgid "Enter desired FPS"
227-
msgstr "输入所需的 FPS"
228-
229-
msgid "Results (remember to replace N with your game speed number!):"
230-
msgstr "结果(别忘了把 N 替换成你的游戏速度编号):"
231-
232226
msgid "Include files"
233227
msgstr "包含文件"
234228

0 commit comments

Comments
 (0)