From 2e4c3e5064ff3771ef3dd59863de0bc5dad74b3b Mon Sep 17 00:00:00 2001 From: Benny Guo Date: Thu, 24 Aug 2023 04:06:40 +0800 Subject: [PATCH] feat: added traditional chinese language support --- src/App.vue | 6 +- src/components/Comment.vue | 9 +- src/components/Header/src/Controls.vue | 15 ++- src/components/Header/src/Navigation.vue | 40 ++------ src/components/MobileMenu.vue | 30 ++++-- src/hooks/usePageTitle.ts | 5 +- src/locales/languages/{cn.json => zh-CN.json} | 0 src/locales/languages/zh-TW.json | 91 +++++++++++++++++++ src/models/ThemeConfig.class.ts | 27 ++++-- src/stores/app.ts | 12 +-- src/utils/aurora-dia/index.ts | 2 +- src/utils/comments/github-api.ts | 22 ++++- src/utils/comments/leancloud-api.ts | 10 +- src/utils/comments/twikoo-api.ts | 5 +- src/utils/index.ts | 18 +++- src/views/Page.vue | 5 +- tests/unit/utils/formatTime.spec.ts | 16 ++-- 17 files changed, 219 insertions(+), 94 deletions(-) rename src/locales/languages/{cn.json => zh-CN.json} (100%) create mode 100644 src/locales/languages/zh-TW.json diff --git a/src/App.vue b/src/App.vue index c9676de0..d2e5f819 100644 --- a/src/App.vue +++ b/src/App.vue @@ -114,15 +114,15 @@ export default defineComponent({ if (appStore.themeConfig.plugins.copy_protection.enable) { const locale = appStore.locale const linkPlaceholder = - locale === 'cn' + locale === 'zh-CN' ? appStore.themeConfig.plugins.copy_protection.link.cn : appStore.themeConfig.plugins.copy_protection.link.en const authorPlaceholder = - locale === 'cn' + locale === 'zh-CN' ? appStore.themeConfig.plugins.copy_protection.author.cn : appStore.themeConfig.plugins.copy_protection.author.en const licensePlaceholder = - locale === 'cn' + locale === 'zh-CN' ? appStore.themeConfig.plugins.copy_protection.license.cn : appStore.themeConfig.plugins.copy_protection.license.en diff --git a/src/components/Comment.vue b/src/components/Comment.vue index 591644f2..75406a47 100644 --- a/src/components/Comment.vue +++ b/src/components/Comment.vue @@ -23,11 +23,6 @@ import { githubInit } from '@/utils/comments/github-api' import { valineInit } from '@/utils/comments/valine-api' import { walineInit } from '@/utils/comments/waline-api' -const languages: Record = { - en: 'en', - cn: 'zh' -} - export default defineComponent({ name: 'ObComment', props: { @@ -151,7 +146,7 @@ export default defineComponent({ waline = walineInit({ serverURL, - lang: languages[appStore.locale ?? 'en'], + lang: appStore.locale ?? 'en', login, reaction, meta, @@ -181,7 +176,7 @@ export default defineComponent({ (newLocale, oldLocale) => { if (waline && newLocale !== undefined && newLocale !== oldLocale) { waline.update({ - lang: languages[newLocale] + lang: newLocale }) } } diff --git a/src/components/Header/src/Controls.vue b/src/components/Header/src/Controls.vue index 71573df3..74dd6f0d 100644 --- a/src/components/Header/src/Controls.vue +++ b/src/components/Header/src/Controls.vue @@ -34,15 +34,19 @@ width="1.2rem" height="1.2rem" /> - 中文 - EN + 简体 + 繁體 + En English - - 中文 + + 简体 + + + 繁體 @@ -98,6 +102,7 @@ import SearchModal from '@/components/SearchModal.vue' import { useSearchStore } from '@/stores/search' import SvgIcon from '@/components/SvgIcon/index.vue' import { useNavigatorStore } from '@/stores/navigator' +import { Locales } from '@/models/ThemeConfig.class' export default defineComponent({ name: 'Controls', @@ -121,7 +126,7 @@ export default defineComponent({ const navigatorStore = useNavigatorStore() const ballProgress = toRefs(props).scrollProgress - const handleClick = (name: string): void => { + const handleClick = (name: Locales): void => { appStore.changeLocale(name) } diff --git a/src/components/Header/src/Navigation.vue b/src/components/Header/src/Navigation.vue index 4b67f673..6c0f7e4c 100644 --- a/src/components/Header/src/Navigation.vue +++ b/src/components/Header/src/Navigation.vue @@ -12,17 +12,8 @@ v-if="route.children && route.children.length === 0" :data-menu="route.name" > - - {{ route.i18n.cn }} - - - {{ route.i18n.en }} + + {{ route.i18n[locale] }} {{ route.name }} @@ -32,17 +23,8 @@ v-else class="nav-link text-sm block px-1.5 py-0.5 rounded-md relative uppercase" > - - {{ route.i18n.cn }} - - - {{ route.i18n.en }} + + {{ route.i18n[locale] }} {{ route.name }} @@ -51,17 +33,8 @@ :key="sub.path" :name="sub.path" > - - {{ sub.i18n.cn }} - - - {{ sub.i18n.en }} + + {{ sub.i18n[locale] }} {{ sub.name }} @@ -100,6 +73,7 @@ export default defineComponent({ } return { + locale: computed(() => appStore.locale), routes: computed(() => appStore.themeConfig.menu.menus), pushPage, te, diff --git a/src/components/MobileMenu.vue b/src/components/MobileMenu.vue index 2593c531..607727f3 100644 --- a/src/components/MobileMenu.vue +++ b/src/components/MobileMenu.vue @@ -63,9 +63,15 @@ > - {{ route.i18n.cn }} + {{ route.i18n['zh-CN'] }} + + + {{ route.i18n['zh-TW'] }} + {{ route.i18n['zh-CN'] }} + + - {{ route.i18n.cn }} + {{ route.i18n['zh-TW'] }} + {{ sub.i18n['zh-CN'] }} + + - {{ sub.i18n.cn }} + {{ sub.i18n['zh-TW'] }} { - const currentLocale = (locale ?? appStore.locale) === 'cn' ? 'cn' : 'en' + const updateTitle = (locale?: Locales | undefined) => { + const currentLocale: Locales = locale ?? appStore.locale const menuName = String(route.name) const routeInfo = appStore.themeConfig.menu.menus[ diff --git a/src/locales/languages/cn.json b/src/locales/languages/zh-CN.json similarity index 100% rename from src/locales/languages/cn.json rename to src/locales/languages/zh-CN.json diff --git a/src/locales/languages/zh-TW.json b/src/locales/languages/zh-TW.json new file mode 100644 index 00000000..21329b31 --- /dev/null +++ b/src/locales/languages/zh-TW.json @@ -0,0 +1,91 @@ +{ + "menu": { + "home": "首頁", + "about": "關於", + "archives": "歸檔", + "categories": "分類", + "tags": "標簽", + "post": "文章", + "message-board": "留言闆", + "search": "搜索結果", + "not-found": "無法找到頁面" + }, + "home": { + "recommended": "推薦文章" + }, + "titles": { + "articles": "文章列錶", + "about": "關於我", + "category_list": "分類", + "tag_list": "標簽", + "toc": "文章目錄", + "comment": "評論區", + "recent_comment": "最近評論" + }, + "settings": { + "months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "articles": "文章", + "categories": "分類", + "tags": "標簽", + "words": "文字", + "visitor_count": "總共訪客數", + "visit_count": "總共訪問數", + "button-all": "全部", + "paginator": { + "newer": "新的", + "older": "以往", + "prev": "上一篇更回味", + "next": "下一篇更精彩" + }, + "more-tags": "查看更多", + "admin-user": "博主", + "shared-on": "發佈於", + "recently-search": "最近搜索", + "search-result": "一共找到 [total] 個結果", + "no-recent-search": "沒有最近搜索記錄。", + "no-search-result": "沒有找到任何記錄。", + "cmd-to-select": "查看", + "cmd-to-navigate": "選擇", + "cmd-to-close": "關閉", + "searched-by": "搜索引擎", + "tips-back-to-top": "返回頂部", + "tips-open-menu": "打開菜單", + "tips-back-to-home": "返回首頁", + "tips-open-search": "打開搜索", + "default-category": "文章", + "default-tag": "未分類", + "empty-tag": "目前沒有標簽", + "empty-recent-comments": "目前沒有最新評論", + "pinned": "置頂", + "featured": "推薦", + "page-views-value": "瀏覽次數:", + "site-running-for": "建站天數:", + "site-running-for-unit": "天", + "links": "友情鏈接", + "links-slogan": "與無數博主共同進步", + "links-random-visit": "隨機訪問", + "links-apply": "申請友鏈", + "links-badge-personal": "個人", + "links-badge-personal-desc": "記錄關於自己的點點滴滴", + "links-badge-tech": "技術", + "links-badge-tech-desc": "技術為主的博主們", + "links-badge-designer": "設計", + "links-badge-designer-desc": "設計為主的博主們", + "links-badge-vip": "贊助者", + "links-badge-vip-desc": "最近贊助本站的友友們", + "notification-random-jump": "正在隨機挑選一位幸運博主" + } +} diff --git a/src/models/ThemeConfig.class.ts b/src/models/ThemeConfig.class.ts index d6cceae4..bfeae33a 100644 --- a/src/models/ThemeConfig.class.ts +++ b/src/models/ThemeConfig.class.ts @@ -78,7 +78,8 @@ export class ThemeMenu implements ObMenu { name: 'Home', path: '/', i18n: { - cn: '首页', + 'zh-CN': '首页', + 'zh-TW': '首頁', en: 'Home' } }) @@ -95,7 +96,8 @@ export class ThemeMenu implements ObMenu { name: 'About', path: '/about', i18n: { - cn: '关于', + 'zh-CN': '关于', + 'zh-TW': '關於', en: 'About' } }, @@ -103,7 +105,8 @@ export class ThemeMenu implements ObMenu { name: 'Archives', path: '/archives', i18n: { - cn: '归档', + 'zh-CN': '归档', + 'zh-TW': '歸檔', en: 'Archives' } }, @@ -111,7 +114,8 @@ export class ThemeMenu implements ObMenu { name: 'Tags', path: '/tags', i18n: { - cn: '标签', + 'zh-CN': '标签', + 'zh-TW': '標簽', en: 'Tags' } }, @@ -119,7 +123,8 @@ export class ThemeMenu implements ObMenu { name: 'Links', path: '/links', i18n: { - cn: '友情链接', + 'zh-CN': '友情链接', + 'zh-TW': '友情鏈接', en: 'Friend Links' } } @@ -151,13 +156,21 @@ export class ThemeMenu implements ObMenu { } } +enum LocalesTypes { + en, + 'zh-CN', + 'zh-TW' +} + +export type Locales = keyof typeof LocalesTypes + export class Menu { /** Name of the menu */ name = '' /** Vue router path for the menu */ path = '' /** Translation key for vue-i18n */ - i18n: { cn?: string; en?: string } = {} + i18n: Partial> = {} /** Sub menus */ children: Menu[] = [] @@ -382,7 +395,7 @@ export class Site { /** Website description (used in the header meta tag) */ description = '' /** Blog's default language */ - language = 'en' + language: Locales = 'en' /** Allow use to change blog's locale */ multi_language = true /** Site logo or brand logo */ diff --git a/src/stores/app.ts b/src/stores/app.ts index 5e69e74d..66bd01fd 100644 --- a/src/stores/app.ts +++ b/src/stores/app.ts @@ -1,7 +1,7 @@ import { defineStore } from 'pinia' import Cookies from 'js-cookie' import i18n from '@/locales/index' -import { ThemeConfig } from '@/models/ThemeConfig.class' +import { Locales, ThemeConfig } from '@/models/ThemeConfig.class' import { HexoConfig } from '@/models/HexoConfig.class' import { fetchHexoConfig, fetchStatistic } from '@/api' import { Statistic } from '@/models/Statistic.class' @@ -45,7 +45,7 @@ export const useAppStore = defineStore('app', { ? String(Cookies.get('theme')) : getSystemMode(), /** Current locale of the application */ - locale: Cookies.get('locale') ? Cookies.get('locale') : 'en', + locale: (Cookies.get('locale') as Locales) ?? 'en', /** Hexo theme config data */ themeConfig: new ThemeConfig(), /** Hexo engine's config data */ @@ -66,8 +66,8 @@ export const useAppStore = defineStore('app', { openSearchModal: false }), getters: { - getTheme: (state) => state.theme, - getAppLoading: (state) => state.appLoading + getTheme: state => state.theme, + getAppLoading: state => state.appLoading }, actions: { /** Fetching Hexo and Hexo theme's config data */ @@ -107,7 +107,7 @@ export const useAppStore = defineStore('app', { setTheme(this.theme) }, /** Changing the local of the app */ - changeLocale(locale: string) { + changeLocale(locale: Locales) { Cookies.set('locale', locale) this.locale = locale i18n.global.locale.value = locale @@ -116,7 +116,7 @@ export const useAppStore = defineStore('app', { * Setting the default locale of the app base on _config * @remarks If the user had choose a locale before, this default value will be ignored. */ - setDefaultLocale(locale: string) { + setDefaultLocale(locale: Locales) { if (Cookies.get('locale')) return this.changeLocale(locale) }, diff --git a/src/utils/aurora-dia/index.ts b/src/utils/aurora-dia/index.ts index 25d5b973..cdca24ed 100644 --- a/src/utils/aurora-dia/index.ts +++ b/src/utils/aurora-dia/index.ts @@ -384,7 +384,7 @@ class AuroraBotSoftware { } showQuote() { - if (this.config.locale === 'cn') { + if (this.config.locale === 'zh-CN' || this.config.locale === 'zh-TW') { this.getHitokoto() } } diff --git a/src/utils/comments/github-api.ts b/src/utils/comments/github-api.ts index 6bb6d1fd..650d2497 100644 --- a/src/utils/comments/github-api.ts +++ b/src/utils/comments/github-api.ts @@ -8,6 +8,7 @@ declare const Gitalk: any import request from '@/utils/external-request' import { AxiosResponse } from 'axios' import { formatTime, filterHTMLContent, RecentComment } from '@/utils' +import { Locales } from '@/models/ThemeConfig.class' const COMMENT_CACHE_KEY = 'github-comment-cache-key' const GITHUB_API_URL = 'https://api.github.com/repos' @@ -27,7 +28,7 @@ export type GithubAttributes = { * Locale request * @default 'en' */ - lang?: string + lang?: Locales } interface GithubCommentsInterface { @@ -85,9 +86,19 @@ export const githubInit = ({ gitalk.render('gitalk-container') } +interface GithubConfigs { + repo: string + owner: string + clientId: string + clientSecret: string + admin: string + authorizationToken: string + lang: Locales +} + export class GithubComments implements GithubCommentsInterface { commentUrlCount = 0 - configs = { + configs: GithubConfigs = { repo: '', owner: '', clientId: '', @@ -270,7 +281,7 @@ export class GithubComment implements RecentComment { } // Skip filters if it's cache data. if (!cachedData) { - const lang = options && options.lang ? 'en' : 'cn' + const lang = options?.lang ?? 'en' this.filterBody() this.transformTime(lang) } @@ -344,10 +355,11 @@ export class GithubComment implements RecentComment { * * eg. `10 minutes ago.` */ - transformTime(lang: 'en' | 'cn'): void { + transformTime(lang: Locales): void { const templates = { en: 'commented [TIME]', - cn: '[TIME]评论了' + 'zh-CN': '[TIME]评论了', + 'zh-TW': '[TIME]評論了' } this.created_at = formatTime(this.created_at, { diff --git a/src/utils/comments/leancloud-api.ts b/src/utils/comments/leancloud-api.ts index 9f0f9c27..098cf9cb 100644 --- a/src/utils/comments/leancloud-api.ts +++ b/src/utils/comments/leancloud-api.ts @@ -12,6 +12,7 @@ import { formatTime, filterHTMLContent, RecentComment } from '@/utils' import pack from '../../../package.json' import { getGravatar, getGravatarUrl } from './gravatar' +import { Locales } from '@/models/ThemeConfig.class' const VERSION = pack.version let AV_INITIALIZED = false @@ -294,7 +295,7 @@ export class LeanCloudComment implements RecentComment { * @param raw Raw data from LeanCloud API * @param options Additional params */ - constructor(raw?: { [key: string]: any }, lang?: string) { + constructor(raw?: { [key: string]: any }, lang?: Locales) { if (raw) { let cachedData = false for (const key of Object.keys(this)) { @@ -306,7 +307,7 @@ export class LeanCloudComment implements RecentComment { // Skip filters if it's cache data. if (!cachedData) { - const language = lang === 'en' || lang === 'cn' ? lang : 'en' + const language = lang ?? 'en' this.filterBody() this.transformTime(language) } @@ -326,10 +327,11 @@ export class LeanCloudComment implements RecentComment { * * eg. `10 minutes ago.` */ - transformTime(lang: 'en' | 'cn'): void { + transformTime(lang: Locales): void { const templates = { en: 'commented [TIME]', - cn: '[TIME]评论了' + 'zh-CN': '[TIME]评论了', + 'zh-TW': '[TIME]評論了' } this.created_at = formatTime(this.created_at, { diff --git a/src/utils/comments/twikoo-api.ts b/src/utils/comments/twikoo-api.ts index 86a2e333..83d385b0 100644 --- a/src/utils/comments/twikoo-api.ts +++ b/src/utils/comments/twikoo-api.ts @@ -88,7 +88,10 @@ export class TwikooComments { } mapComment(comment: TwikooComment, gravatarUrl: string): RecentComment { - const timezoneDiff = this.configs.lang === 'cn' ? 8 * 1000 * 60 * 60 : 0 + const timezoneDiff = + this.configs.lang === 'zh-CN' || this.configs.lang === 'zh-TW' + ? 8 * 1000 * 60 * 60 + : 0 const createdAt = formatTime( new Date(Number(comment.created) - timezoneDiff).toISOString() ) diff --git a/src/utils/index.ts b/src/utils/index.ts index 999cd06a..e2d8eac6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,5 @@ +import { Locales } from '@/models/ThemeConfig.class' + export interface RecentComment { id: number body: string @@ -23,13 +25,13 @@ export interface RecentComment { */ export function formatTime( time: number | string, - options?: { template?: string; lang?: string } + options?: { template?: string; lang?: Locales } ): string { - const configs = { + const configs: { template: string; lang: Locales } = { template: '[TIME]', lang: 'en' } - const languages: { [lang: string]: { [type: string]: string } } = { + const languages: Record = { en: { seconds: 'just seconds ago', minutes: ' minutes ago', @@ -38,13 +40,21 @@ export function formatTime( months: ' months ago', years: ' years ago' }, - cn: { + 'zh-CN': { seconds: '刚刚', minutes: ' 分钟前', hours: ' 小时前', days: ' 天前', months: ' 个月前', years: ' 年前' + }, + 'zh-TW': { + seconds: '剛剛', + minutes: ' 分鐘前', + hours: ' 小時前', + days: ' 天前', + months: ' 個月前', + years: ' 年前' } } diff --git a/src/views/Page.vue b/src/views/Page.vue index b742969c..64c69683 100644 --- a/src/views/Page.vue +++ b/src/views/Page.vue @@ -34,6 +34,7 @@ import PageContent from '@/components/PageContent.vue' import Breadcrumbs from '@/components/Breadcrumbs.vue' import Comment from '@/components/Comment.vue' import useCommentPlugin from '@/hooks/useCommentPlugin' +import { Locales } from '@/models/ThemeConfig.class' declare const Prism: any @@ -62,8 +63,8 @@ export default defineComponent({ Prism.highlightAll() } - const updateTitle = (locale: string | undefined) => { - const currentLocale = locale === 'cn' ? 'cn' : 'en' + const updateTitle = (locale?: Locales) => { + const currentLocale = locale ?? 'en' const routeInfo = appStore.themeConfig.menu.menus[String(route.params.slug)] pageTitle.value = diff --git a/tests/unit/utils/formatTime.spec.ts b/tests/unit/utils/formatTime.spec.ts index c8542369..fabb94ff 100644 --- a/tests/unit/utils/formatTime.spec.ts +++ b/tests/unit/utils/formatTime.spec.ts @@ -18,7 +18,7 @@ describe('Utils: formatTime', () => { }) it('just now - cn', () => { - expect(formatTime(+new Date() - 1, { lang: 'cn' })).toBe('刚刚') + expect(formatTime(+new Date() - 1, { lang: 'zh-CN' })).toBe('刚刚') }) it('two minutes ago - en', () => { @@ -26,7 +26,7 @@ describe('Utils: formatTime', () => { }) it('two minutes ago - cn', () => { - expect(formatTime(+new Date() - 60 * 2 * 1000, { lang: 'cn' })).toBe( + expect(formatTime(+new Date() - 60 * 2 * 1000, { lang: 'zh-CN' })).toBe( '2分钟前' ) }) @@ -36,7 +36,7 @@ describe('Utils: formatTime', () => { }) it('two hours ago - cn', () => { - expect(formatTime(+new Date() - 3600 * 2 * 1000, { lang: 'cn' })).toBe( + expect(formatTime(+new Date() - 3600 * 2 * 1000, { lang: 'zh-CN' })).toBe( '2小时前' ) }) @@ -46,9 +46,9 @@ describe('Utils: formatTime', () => { }) it('two days ago - cn', () => { - expect(formatTime(+new Date() - 3600 * 24 * 2 * 1000, { lang: 'cn' })).toBe( - '2天前' - ) + expect( + formatTime(+new Date() - 3600 * 24 * 2 * 1000, { lang: 'zh-CN' }) + ).toBe('2天前') }) it('two months ago - en', () => { @@ -59,7 +59,7 @@ describe('Utils: formatTime', () => { it('two months ago - cn', () => { expect( - formatTime(+new Date() - 3600 * 24 * 30 * 2 * 1000, { lang: 'cn' }) + formatTime(+new Date() - 3600 * 24 * 30 * 2 * 1000, { lang: 'zh-CN' }) ).toBe('2个月前') }) @@ -71,7 +71,7 @@ describe('Utils: formatTime', () => { it('two years ago - cn', () => { expect( - formatTime(+new Date() - 3600 * 24 * 365 * 2 * 1000, { lang: 'cn' }) + formatTime(+new Date() - 3600 * 24 * 365 * 2 * 1000, { lang: 'zh-CN' }) ).toBe('2年前') })