Skip to content

Commit

Permalink
Merge pull request #18 from libondev/dev-d
Browse files Browse the repository at this point in the history
feat: 增加多语言支持
  • Loading branch information
libondev authored Apr 8, 2024
2 parents 12aa534 + 977851d commit 38d8f1d
Show file tree
Hide file tree
Showing 13 changed files with 222 additions and 30 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
- [ ] 彩色方块
- [x] 游戏音效
- [x] 游戏界面增加键盘操作
- [ ] i18n
- [x] i18n
- [ ] 积分商店,积分收集、兑换
- [ ] 道具系统

Expand Down
2 changes: 2 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script lang="ts" setup>
import { useI18N } from './composables/use-i18n'
import { provideGameSounds } from '@/composables/use-game-sounds'
provideGameSounds()
useI18N()
</script>

<template>
Expand Down
23 changes: 21 additions & 2 deletions src/components/NavHeader.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
<script lang="ts" setup>
import { gameSoundsInjectionKey } from '@/composables/use-game-sounds'
import { i18NInjectionKey } from '@/composables/use-i18n'
import type { Language } from '@/composables/use-local-cache'
import { languages } from '@/config/game'
const {
enableSounds,
toggleSounds,
} = inject(gameSoundsInjectionKey)!
const { lang, setLanguage, $t } = inject(i18NInjectionKey)!
</script>

<template>
<header class="mb-4 px-4 sm:px-6 border-b dark:border-gray-700 flex items-center justify-between">
<h1 class="text-lg font-medium">
<RouterLink to="/" class="sm:after:content-[attr(data-text)]" data-text="(记忆方块)">
Memory Block
<RouterLink to="/">
{{ $t('memory-block', 'Memory Block') }}
</RouterLink>
</h1>

<div class="pt-2 flex items-center gap-2">
<select
class="inline-block h-8 mb-2 px-3 select-none rounded-lg text-sm border-[rgba(0,0,0,.2)] border-x-[1.5px] border-t-[1.5px] border-b-4"
:value="lang"
@change="e => setLanguage((e.target as HTMLSelectElement).value as Language)"
>
<option
v-for="option of languages"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>

<Button @click="toggleSounds()">
<i class="block" :class="enableSounds ? 'i-solar-volume-loud-broken' : 'i-solar-volume-cross-broken text-red-500'" />
</Button>
Expand Down
60 changes: 60 additions & 0 deletions src/composables/use-i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { ShallowRef } from 'vue'
import { type Language, getLanguage, setLanguage } from './use-local-cache'
import zhCN from '@/locale/zh-CN.json'

interface I18N {
lang: ShallowRef<Language>
setLanguage: (language: Language) => void
$t: (key: string, fallback?: string) => string
}

export const i18NInjectionKey = Symbol('i18n') as InjectionKey<I18N>

function loadLanguage(lang: Language) {
return import.meta.glob(`@/locale/*.json`)[`/src/locale/${lang}.json`]()
}

export function useI18N() {
const lang = shallowRef<Language>('zh-CN')

const messages = ref<Record<Language, null | Record<string, string>>>({
'zh-CN': zhCN,
'en-US': null,
'ja-JP': null,
})

const msg = computed(() => messages.value[lang.value] ?? messages.value['zh-CN'])

const _setLang = (language: Language) => {
lang.value = language
setLanguage(language)

if (!messages.value[language]) {
loadLanguage(language).then((res: any) => {
messages.value[language] = res.default as Record<string, string>
})
}
}

getLanguage().then((val) => {
if (val) {
_setLang(val)
}
})

const $t = (key: string, fallback: string = ''): string => {
return msg.value?.[key] ?? fallback
}

provide(i18NInjectionKey, {
lang,
setLanguage: _setLang,
$t,
})

return {
lang,
setLanguage: _setLang,
$t,
}
}
12 changes: 12 additions & 0 deletions src/composables/use-local-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ export interface RecordItem {
endTime: string
}

export type Language = 'zh-CN' | 'en-US' | 'ja-JP'

export const RECORD_KEY = 'record'

const HIGHEST_SCORE_KEY = 'highestScore.'

const LANGUAGE_KEY = 'memoryBlockLanguage'

// 获取最高分
export function getHighestScoreInHistory(level: GameLevel) {
return localforage.getItem<number>(HIGHEST_SCORE_KEY + level, v => v ?? 0)
Expand Down Expand Up @@ -46,3 +50,11 @@ export async function getAllRecordsFromStore() {

return _records
}

// 获取语言
export const getLanguage = () => localforage.getItem<Language>(LANGUAGE_KEY, v => v ?? 'zh-CN')

// 设置语言
export function setLanguage(lang: Language = 'zh-CN') {
localforage.setItem(LANGUAGE_KEY, lang)
}
16 changes: 11 additions & 5 deletions src/config/game.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export const GAME_LEVELS = {
easy: { code: 'easy', type: 'default', en: 'Easy Level', zh: '简单难度', path: '/game/easy' },
normal: { code: 'normal', type: 'primary', en: 'Normal Level', zh: '中等难度', path: '/game/normal' },
master: { code: 'master', type: 'warning', en: 'Master Level', zh: '困难难度', path: '/game/master' },
expert: { code: 'expert', type: 'danger', en: 'Expert Level', zh: '专家难度', path: '/game/expert' },
custom: { code: 'custom', type: 'custom', en: 'Practice Mode', zh: '练习模式', path: '/settings/custom' },
easy: { code: 'easy', type: 'default', en: 'Easy Level', zh: '简单难度', ja: '簡単なレベル', path: '/game/easy' },
normal: { code: 'normal', type: 'primary', en: 'Normal Level', zh: '中等难度', ja: '中レベル', path: '/game/normal' },
master: { code: 'master', type: 'warning', en: 'Master Level', zh: '困难难度', ja: 'マスターレベル', path: '/game/master' },
expert: { code: 'expert', type: 'danger', en: 'Expert Level', zh: '专家难度', ja: 'エキスパートレベル', path: '/game/expert' },
custom: { code: 'custom', type: 'custom', en: 'Practice Mode', zh: '练习模式', ja: '練習モード', path: '/settings/custom' },
} as const

export const LEVEL_GRIDS = {
Expand Down Expand Up @@ -61,3 +61,9 @@ export const LEVEL_GRIDS = {
export type GameLevel = {
[K in keyof typeof GAME_LEVELS]: typeof GAME_LEVELS[K]['code'];
}[keyof typeof GAME_LEVELS]

export const languages = [
{ label: '中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' },
{ label: '日本語', value: 'ja-JP' },
]
28 changes: 28 additions & 0 deletions src/locale/en-US.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

{
"memory-block": "Memory block",
"custom-levels": "Custom levels",
"number-of-grids": "Number of grids",
"minimum-blocks": "Minimum number of generated blocks",
"maximum-blocks": "Maximum number of generated blocks",
"hp": "HP",
"second": "second",
"setup-completed": "Setup completed, start the game",

"score": "Score",
"start-time": "Start time",
"end-time": "Ends time",
"using-time": "Using time",

"game-over": "Game over",
"start": "Start",
"again": "Again",
"clear": "Clear",
"selected": "Selected",
"continue": "Continue",

"memory-time": "Memory time before the start of each round",
"configuration-integer-gt": "Configuration can only be an integer greater than 1",
"select-one-first": "Please select at least one block first",
"remember-block-locations": "Please remember the following block locations"
}
27 changes: 27 additions & 0 deletions src/locale/ja-JP.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"memory-block": "メモリ ブロック",
"custom-levels": "カスタム レベル",
"number-of-grids": "グリッドの数",
"minimum-blocks": "生成されるブロックの最小数",
"maximum-blocks": "生成されるブロックの最大数",
"hp": "HP",
"second": "",
"setup-completed": "セットアップが完了しました。ゲームを開始します",

"score": "スコア",
"start-time": "開始時刻",
"end-time": "終了時刻",
"using-time": "使用時間",

"game-over": "ゲームオーバー",
"start": "ゲーム開始",
"again": "また",
"clear": "選択をクリアします",
"selected": "選択済み",
"continue": "継続",

"memory-time": "各ラウンド開始前の記憶時間",
"configuration-integer-gt": "構成には 1 より大きい整数のみを指定できます",
"select-one-first": "最初に少なくとも 1 つのブロックを選択してください",
"remember-block-locations": "次のブロックの場所を覚えておいてください"
}
27 changes: 27 additions & 0 deletions src/locale/zh-CN.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"memory-block": "记忆方块",
"custom-levels": "自定义关卡",
"number-of-grids": "网格数量",
"minimum-blocks": "最小生成方块数",
"maximum-blocks": "最大生成方块数",
"hp": "生命值",
"second": "",
"setup-completed": "设置完成,开始游戏",

"score": "得分",
"start-time": "开始于",
"end-time": "结束于",
"using-time": "用时",

"game-over": "游戏结束",
"start": "游戏开始",
"again": "再来一次",
"clear": "清空选中",
"selected": "选好了",
"continue": "继续",

"memory-time": "每回合开始前的记忆时间",
"configuration-integer-gt": "配置只能为大于 1 的整数",
"select-one-first": "请先选择至少一个方块",
"remember-block-locations": "请记住以下方块位置"
}
14 changes: 8 additions & 6 deletions src/views/game/[level].vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import { useGameStatus } from '@/composables/use-game-status'
import { useGameScore } from '@/composables/use-game-score'
import { useCheckedBlocks } from '@/composables/use-checked-blocks'
import { setHighestScoreInHistory } from '@/composables/use-local-cache'
import { i18NInjectionKey } from '@/composables/use-i18n'
type GameStatus = 'over' | 'pause' | 'playing' | 'previewing'
const { $t } = inject(i18NInjectionKey)!
const _gameState = shallowRef<GameStatus>('previewing')
const gameStatus = computed(() => ({
Expand Down Expand Up @@ -111,7 +114,7 @@ function onCheckResult() {
const { matched, blocks } = getAllCheckedResult()
if (!matched && !blocks.length) {
useToast('请先选择至少一个方块')
useToast($t('select-one-first', '请先选择至少一个方块'))
return
}
Expand Down Expand Up @@ -158,7 +161,7 @@ function gameOver() {
markAllWrongBlocks()
setGameStatus('over')
useToastError('游戏结束')
useToastError($t('game-over', '游戏结束'))
// 如果分数比历史最高分高, 更新历史最高分, 并播放纸屑
if (gameScore.value > highestScore.value) {
Expand Down Expand Up @@ -289,7 +292,7 @@ onBeforeUnmount(() => {
<main class="h-full flex items-center justify-center">
<div>
<h2 class="w-full flex items-center justify-center text-xl font-mono">
{{ gameStatus.previewing ? '请记住以下方块位置' : gameStatus.over ? '游戏结束' : '游戏开始' }}<template v-if="countdown">
{{ gameStatus.previewing ? $t('remember-block-locations', '请记住以下方块位置') : gameStatus.over ? $t('game-over', '游戏结束') : $t('start', '游戏开始') }}<template v-if="countdown">
({{ countdown }})
</template>
</h2>
Expand Down Expand Up @@ -357,17 +360,16 @@ onBeforeUnmount(() => {
:disabled="gameStatus.previewing || gameStatus.pause"
@click="onResetBlocks"
>
{{ gameStatus.over ? '再来一次' : '清空选中' }}
{{ gameStatus.over ? $t('again', '再来一次') : $t('clear', '清空选中') }}
</Button>

<Button
v-show="!gameStatus.over"
:disabled="gameStatus.previewing"
:type="gameStatus.pause ? 'warning' : 'primary'"
class="w-[72px]"
@click="onCheckResult"
>
{{ gameStatus.pause ? '继续' : '选好了' }}
{{ gameStatus.pause ? $t('continue', '继续') : $t('selected', '选好了') }}
</Button>
</div>
</div>
Expand Down
11 changes: 7 additions & 4 deletions src/views/index.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
<script setup lang="ts">
import { GAME_LEVELS } from '@/config/game'
import { i18NInjectionKey } from '@/composables/use-i18n'
const { lang, $t } = inject(i18NInjectionKey)!
const key = computed(() => lang.value.split('-')[0])
</script>

<template>
<div className="h-full flex flex-col items-center justify-center">
<h2 className="-mt-32 text-center text-3xl text-medium font-mono">
Memory Block
<br>
记忆方块
{{ $t('memory-block', '记忆方块') }}
</h2>

<div className="mt-20 flex items-center flex-col gap-2">
<Button v-for="btn of GAME_LEVELS" :key="btn.path" :to="btn.path" :type="btn.type" as="RouterLink">
{{ btn.zh }}
{{ (btn as any)[key] }}
</Button>
</div>
</div>
Expand Down
11 changes: 7 additions & 4 deletions src/views/record/[date].vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<script lang="ts" setup>
import type { RecordItem } from '@/composables/use-local-cache'
import { getTargetDateRecords } from '@/composables/use-local-cache'
import { i18NInjectionKey } from '@/composables/use-i18n'
const { $t } = inject(i18NInjectionKey)!
const route = useRoute()
Expand All @@ -16,10 +19,10 @@ onMounted(async () => {
<ul class="max-w-[500px] mx-auto flex flex-col-reverse">
<li v-for="record of records" :key="record.startTime" class="mb-2 py-2">
<LevelTag :level="record.level" class="mr-2" />
<span>得分: {{ record.score }}</span>
<span>开始于: {{ record.startTime }}</span>
<span>结束于: {{ record.endTime }}</span>
<span>用时: {{ record.durations }}</span>
<span>{{ $t('score', '得分') }}: {{ record.score }}</span>
<span>{{ $t('start-time', '开始于') }}: {{ record.startTime }}</span>
<span>{{ $t('end-time', '结束于') }}: {{ record.endTime }}</span>
<span>{{ $t('using-time', '用时') }}: {{ record.durations }}</span>
</li>
</ul>
</div>
Expand Down
Loading

0 comments on commit 38d8f1d

Please sign in to comment.