diff --git a/README.md b/README.md index 947320ca8..f0d868b1b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ Timer can - support to set the maximum time for browsing a specific website each day to help you keey away from time wasting. - support to export files formatted _.csv_ and _.json_. - support dark mode. -- support to sync data with Github Gist across serveral browser clients. +- support to sync data with GitHub Gist across serveral browser clients. +- support to count the time of any URLs described by Ant Expressions, e.g. github.com/sheepzh/timer/** ## Install diff --git a/package.json b/package.json index f8dcac325..b71edcc80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.7.0", + "version": "1.9.0", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { @@ -12,7 +12,11 @@ "test": "jest --env=jsdom", "test-c": "jest --coverage --env=jsdom" }, - "author": "zhy", + "author": { + "name": "zhy", + "email": "returnzhy1996@outlook.com", + "url": "https://www.github.com/sheepzh" + }, "license": "MIT", "devDependencies": { "@crowdin/crowdin-api-client": "^1.22.1", @@ -31,13 +35,14 @@ "eslint": "^8.36.0", "filemanager-webpack-plugin": "^8.0.0", "generate-json-webpack-plugin": "^2.0.0", + "html-webpack-plugin": "^5.5.1", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "mini-css-extract-plugin": "^2.7.5", "node-sass": "^8.0.0", "sass-loader": "^13.2.1", "style-loader": "^3.3.2", - "ts-jest": "^29.0.5", + "ts-jest": "^29.1.0", "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.2", @@ -63,4 +68,4 @@ "engines": { "node": ">=16" } -} \ No newline at end of file +} diff --git a/public/app.html b/public/app.html index 99444ee94..fa7b305eb 100644 --- a/public/app.html +++ b/public/app.html @@ -3,12 +3,10 @@ -
- \ No newline at end of file diff --git a/public/guide.html b/public/guide.html index b0d70fceb..aa3782b78 100644 --- a/public/guide.html +++ b/public/guide.html @@ -7,13 +7,13 @@ - - - - - -
- - - + + + + + +
+ + + \ No newline at end of file diff --git a/public/images/guide/beating.gif b/public/images/guide/beating.gif new file mode 100644 index 000000000..2749cf82e Binary files /dev/null and b/public/images/guide/beating.gif differ diff --git a/public/images/guide/home.png b/public/images/guide/home.png new file mode 100644 index 000000000..d9bad2587 Binary files /dev/null and b/public/images/guide/home.png differ diff --git a/public/images/guide/pin.png b/public/images/guide/pin.png new file mode 100644 index 000000000..a7fbeec38 Binary files /dev/null and b/public/images/guide/pin.png differ diff --git a/public/popup.html b/public/popup.html index 2bdda1a63..3f771cfc4 100644 --- a/public/popup.html +++ b/public/popup.html @@ -10,7 +10,6 @@ - @@ -149,6 +148,5 @@ - \ No newline at end of file diff --git a/script/crowdin/common.ts b/script/crowdin/common.ts index 736ce1503..f6df06ac7 100644 --- a/script/crowdin/common.ts +++ b/script/crowdin/common.ts @@ -11,13 +11,15 @@ export const SOURCE_LOCALE: timer.SourceLocale = 'en' export const ALL_TRANS_LOCALES: timer.Locale[] = [ 'ja', 'zh_TW', + 'pt_PT', ] const CROWDIN_I18N_MAP: Record = { en: 'en', ja: 'ja', - "zh-CN": 'zh_CN', - "zh-TW": 'zh_TW', + 'zh-CN': 'zh_CN', + 'zh-TW': 'zh_TW', + 'pt-PT': 'pt_PT', } const I18N_CROWDIN_MAP: Record = { @@ -25,6 +27,7 @@ const I18N_CROWDIN_MAP: Record = { ja: 'ja', zh_CN: 'zh-CN', zh_TW: 'zh-TW', + pt_PT: 'pt-PT' } export const crowdinLangOf = (locale: timer.Locale) => I18N_CROWDIN_MAP[locale] @@ -34,17 +37,18 @@ export const localeOf = (crowdinLang: CrowdinLanguage) => CROWDIN_I18N_MAP[crowd const IGNORED_FILE: Partial<{ [dir in Dir]: string[] }> = { common: [ // Strings for market - 'meta.ts', + 'meta', // Name of locales - 'locale.ts', + 'locale', ] } -export function isIgnored(dir: Dir, tsFilename: string) { - return !!IGNORED_FILE[dir]?.includes(tsFilename) +export function isIgnored(dir: Dir, fileName: string) { + return !!IGNORED_FILE[dir]?.includes(fileName) } -const MSG_BASE = path.join(__dirname, '..', '..', 'src', 'i18n', 'message') +export const MSG_BASE = path.join(__dirname, '..', '..', 'src', 'i18n', 'message') +export const RSC_FILE_SUFFIX = "-resource.json" /** * Read all messages from source file @@ -58,21 +62,23 @@ export async function readAllMessages(dir: Dir): Promise { - if (!file.endsWith('.ts')) { - return - } - if (file === 'index.ts') { + if (!file.endsWith(RSC_FILE_SUFFIX)) { return } const message = (await import(`@i18n/message/${dir}/${file}`))?.default as Messages - message && (result[file] = message) + const name = file.replace(RSC_FILE_SUFFIX, '') + message && (result[name] = message) return })) return result } /** - * Merge crowdin message into locale codes + * Merge crowdin message into locale resource json files + * + * @param dir dir + * @param filename the name of json file + * @param messages crowdin messages */ export async function mergeMessage( dir: Dir, @@ -95,6 +101,10 @@ export async function mergeMessage( return } const sourceText = sourceItemSet[path] + if (!sourceText) { + // Deleted key + return + } if (!checkPlaceholder(text, sourceText)) { console.error(`Invalid placeholder: dir=${dir}, filename=${filename}, path=${path}, source=${sourceText}, translated=${text}`) return @@ -108,18 +118,7 @@ export async function mergeMessage( } }) - const existFile = fs.readFileSync(filePath, { encoding: 'utf-8' }) - const pattern = /(const|let|var) _default(.*)=\s*\{\s*(\n?.*\n)+\}/ - const patternRes = pattern.exec(existFile) - const existDefault = patternRes?.[0] - if (!existDefault) { - exitWith(`Failed to find: ${pattern} in ${filePath}`) - } - const index = existFile.indexOf(existDefault) - const pre = existFile.substring(0, index) - const suffix = existFile.substring(index + existDefault.length) - const newDefault = generateDefault(existDefault, existMessages) - const newFileContent = pre + newDefault + suffix + const newFileContent = JSON.stringify(existMessages, null, 4) fs.writeFileSync(filePath, newFileContent, { encoding: 'utf-8' }) } @@ -143,42 +142,6 @@ function checkPlaceholder(translated: string, source: string) { return true } -const INDENTATION_UNIT = ' ' - -function generateDefault(existDetault: string, messages: Messages): string { - let codeLines = /(const|let|var) _default(.*)=\s*\{/.exec(existDetault)[0] - codeLines += '\n' - codeLines += generateFieldLines(messages, INDENTATION_UNIT) - codeLines += '\n}' - return codeLines -} - -function generateFieldLines(messages: Object, indentation: string): string { - const lines = [] - Object.entries(messages).forEach(([key, value]) => { - let line = undefined - if (typeof value === 'object') { - const subCodeLines = generateFieldLines(value, indentation + INDENTATION_UNIT) - line = `${indentation}${key}: {\n${subCodeLines}\n${indentation}}` - } else { - const valueText = JSON.stringify(value) - // Use double quotes - .replace(/'/g, '\\\'').replace(/"/g, '\'') - // Replace tab signs - .replace(/\s{4}/g, '') - line = `${indentation}${key}: ${valueText}` - } - lines.push(line) - }) - let codeLines = lines.join(',\n') - if (codeLines) { - // Add comma at the end of last line - codeLines += ',' - } - return codeLines -} - - function fillItem(fields: string[], index: number, obj: Object, text: string) { const field = fields[index] if (index === fields.length - 1) { diff --git a/script/crowdin/crowdin.d.ts b/script/crowdin/crowdin.d.ts index e887914e6..589f2cc9f 100644 --- a/script/crowdin/crowdin.d.ts +++ b/script/crowdin/crowdin.d.ts @@ -36,4 +36,5 @@ type CrowdinLanguage = | 'zh-CN' | 'en' | 'zh-TW' - | 'ja' \ No newline at end of file + | 'ja' + | 'pt-PT' \ No newline at end of file diff --git a/script/crowdin/export-translation.ts b/script/crowdin/export-translation.ts index a44193e4d..ee50d9b09 100644 --- a/script/crowdin/export-translation.ts +++ b/script/crowdin/export-translation.ts @@ -1,6 +1,6 @@ import { SourceFilesModel } from "@crowdin/crowdin-api-client" import { CrowdinClient, getClientFromEnv } from "./client" -import { ALL_DIRS, ALL_TRANS_LOCALES, checkMainBranch, crowdinLangOf, mergeMessage } from "./common" +import { ALL_DIRS, ALL_TRANS_LOCALES, RSC_FILE_SUFFIX, checkMainBranch, crowdinLangOf, mergeMessage } from "./common" async function processFile(client: CrowdinClient, file: SourceFilesModel.File, dir: Dir): Promise { const itemSets: Partial> = {} @@ -9,7 +9,7 @@ async function processFile(client: CrowdinClient, file: SourceFilesModel.File, d const items: ItemSet = await client.downloadTranslations(file.id, lang) items && Object.keys(items).length && (itemSets[locale] = items) } - await mergeMessage(dir, file.name.replace('.json', '.ts'), itemSets) + await mergeMessage(dir, file.name.replace('.json', RSC_FILE_SUFFIX), itemSets) } async function processDir(client: CrowdinClient, branch: SourceFilesModel.Branch, dir: Dir): Promise { diff --git a/script/crowdin/sync-source.ts b/script/crowdin/sync-source.ts index e5bbe27b1..5e8f0128e 100644 --- a/script/crowdin/sync-source.ts +++ b/script/crowdin/sync-source.ts @@ -63,12 +63,12 @@ async function processByDir(client: CrowdinClient, dir: Dir, branch: SourceFiles console.log("Exists file count: " + existFiles.length) const existFileNameMap = groupBy(existFiles, f => f.name, l => l[0]) // 4. create new files - for (const [tsFilename, msg] of Object.entries(messages)) { - if (isIgnored(dir, tsFilename)) { - console.log(`Ignored file: ${dir}/${tsFilename}`) + for (const [fileName, msg] of Object.entries(messages)) { + if (isIgnored(dir, fileName)) { + console.log(`Ignored file: ${dir}/${fileName}`) continue } - const crwodinFilename = tsFilename.replace('.ts', '.json') + const crwodinFilename = fileName + '.json' const fileContent = transMsg(msg[SOURCE_LOCALE]) let existFile = existFileNameMap[crwodinFilename] if (!existFile) { diff --git a/script/crowdin/sync-translation.ts b/script/crowdin/sync-translation.ts index 08b9ebf3d..874cc03a6 100644 --- a/script/crowdin/sync-translation.ts +++ b/script/crowdin/sync-translation.ts @@ -42,16 +42,16 @@ async function processDir(client: CrowdinClient, dir: Dir, branch: SourceFilesMo const files = await client.listFilesByDirectory(directory.id) console.log(`find ${files.length} files of ${dir}`) const fileMap = groupBy(files, f => f.name, l => l[0]) - for (const [tsFilename, message] of Object.entries(messages)) { - console.log(`Start to sync translations of ${dir}/${tsFilename}`) - if (isIgnored(dir, tsFilename)) { - console.log("Ignored file: " + tsFilename) + for (const [fileName, message] of Object.entries(messages)) { + console.log(`Start to sync translations of ${dir}/${fileName}`) + if (isIgnored(dir, fileName)) { + console.log("Ignored file: " + fileName) continue } - const filename = tsFilename.replace('.ts', '.json') - const crowdinFile = fileMap[filename] + const crowdinFileName = fileName + '.json' + const crowdinFile = fileMap[crowdinFileName] if (!crowdinFile) { - console.log(`Failed to find file: dir=${dir}, filename=${tsFilename}`) + console.log(`Failed to find file: dir=${dir}, filename=${fileName}`) continue } @@ -71,9 +71,6 @@ async function main() { const client = getClientFromEnv() const branch = await checkMainBranch(client) - for (let i = 0; i < ALL_DIRS.length; i++) { - - } for (const dir of ALL_DIRS) { await processDir(client, dir, branch) } diff --git a/src/api/chrome/runtime.ts b/src/api/chrome/runtime.ts index 918c8a7c0..a728e5976 100644 --- a/src/api/chrome/runtime.ts +++ b/src/api/chrome/runtime.ts @@ -38,4 +38,13 @@ export function getVersion(): string { export function setUninstallURL(url: string): Promise { return new Promise(resolve => chrome.runtime.setUninstallURL(url, resolve)) +} + +/** + * Get the url of this extension + * + * @param path The path relative to the root directory of this extension + */ +export function getUrl(path: string): string { + return chrome.runtime.getURL(path) } \ No newline at end of file diff --git a/src/api/chrome/tab.ts b/src/api/chrome/tab.ts index 7574efded..da3f4c8cd 100644 --- a/src/api/chrome/tab.ts +++ b/src/api/chrome/tab.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + import { handleError } from "./common" export function getTab(id: number): Promise { @@ -7,6 +14,13 @@ export function getTab(id: number): Promise { })) } +export function getCurrentTab(): Promise { + return new Promise(resolve => chrome.tabs.getCurrent(tab => { + handleError("getCurrentTab") + resolve(tab) + })) +} + export function createTab(param: chrome.tabs.CreateProperties | string): Promise { const prop: chrome.tabs.CreateProperties = typeof param === 'string' ? { url: param } : param return new Promise(resolve => chrome.tabs.create(prop, tab => { @@ -15,6 +29,26 @@ export function createTab(param: chrome.tabs.CreateProperties | string): Promise })) } +/** + * Create one tab after current tab. + * + * Must not be invocked in background.js + */ +export async function createTabAfterCurrent(url: string): Promise { + const tab = await getCurrentTab() + if (!tab) { + // Current tab not found + return createTab(url) + } else { + const { windowId, index: currentIndex } = tab + return createTab({ + url, + windowId, + index: (currentIndex ?? -1) + 1 + }) + } +} + export function listTabs(query?: chrome.tabs.QueryInfo): Promise { query = query || {} return new Promise(resolve => chrome.tabs.query(query, tabs => { diff --git a/src/api/chrome/window.ts b/src/api/chrome/window.ts index b40fc63da..89aa8c4c7 100644 --- a/src/api/chrome/window.ts +++ b/src/api/chrome/window.ts @@ -28,11 +28,7 @@ export function getFocusedNormalWindow(): Promise { } export function getWindow(id: number): Promise { - return new Promise(resolve => - chrome.windows.get(id) - .then(win => resolve(win)) - .catch(_ => resolve(undefined)) - ) + return new Promise(resolve => chrome.windows.get(id, win => resolve(win))) } type _Handler = (windowId: number) => void @@ -41,5 +37,5 @@ export function onNormalWindowFocusChanged(handler: _Handler) { chrome.windows.onFocusChanged.addListener(windowId => { handleError('onWindowFocusChanged') handler(windowId) - }, { windowTypes: ['normal'] }) + }) } \ No newline at end of file diff --git a/src/api/crowdin.ts b/src/api/crowdin.ts index a5299faf4..cbbdf2a1b 100644 --- a/src/api/crowdin.ts +++ b/src/api/crowdin.ts @@ -24,6 +24,12 @@ export type TranslationStatusInfo = { translationProgress: number } +export type MemberInfo = { + username: string + joinedAt: string + avatarUrl: string +} + export async function getTranslationStatus(): Promise { const limit = 500 const auth = `Bearer ${PUBLIC_TOKEN}` @@ -33,4 +39,15 @@ export async function getTranslationStatus(): Promise { }) const data: { data: { data: TranslationStatusInfo }[] } = response.data return data.data.map(i => i.data) +} + +export async function getMembers(): Promise { + const limit = 20 + const auth = `Bearer ${PUBLIC_TOKEN}` + const url = `https://api.crowdin.com/api/v2/projects/${CROWDIN_PROJECT_ID}/members?limit=${limit}` + const response: AxiosResponse = await axios.get(url, { + headers: { "Authorization": auth } + }) + const data: { data: { data: MemberInfo }[] } = response.data + return data.data.map(i => i.data) } \ No newline at end of file diff --git a/src/app/components/analysis/components/filter.ts b/src/app/components/analysis/components/filter.ts index 3d2db2453..07c1f10bf 100644 --- a/src/app/components/analysis/components/filter.ts +++ b/src/app/components/analysis/components/filter.ts @@ -10,23 +10,36 @@ import type { Ref, PropType, VNode } from "vue" import { ElOption, ElSelect, ElTag } from "element-plus" import { ref, h, defineComponent } from "vue" import statService, { HostSet } from "@service/stat-service" +import siteService from "@service/site-service" import { t } from "@app/locale" import SelectFilterItem from "@app/components/common/select-filter-item" import { labelOfHostInfo } from "../util" -async function handleRemoteSearch(queryStr: string, trendDomainOptions: Ref, searching: Ref) { +const calcUniqueKey = ({ host, virtual, merged }: timer.site.SiteInfo) => `${host}${virtual ? 1 : 0}${merged ? 1 : 0}` + +async function handleRemoteSearch(queryStr: string, trendDomainOptions: Ref, searching: Ref) { if (!queryStr) { trendDomainOptions.value = [] return } searching.value = true - const domains: HostSet = await statService.listHosts(queryStr) - const options: timer.site.SiteKey[] = [] - const { origin, merged, virtual } = domains - origin.forEach(host => options.push({ host })) - merged.forEach(host => options.push({ host, merged: true })) - virtual.forEach(host => options.push({ host, virtual: true })) - trendDomainOptions.value = options + + const options: Record = {} + const sites = await siteService.selectAll({ fuzzyQuery: queryStr }) + const hosts: HostSet = await statService.listHosts(queryStr) + + sites.forEach(site => options[calcUniqueKey(site)] = site) + + const { origin, merged, virtual } = hosts + const originSiteInfo: timer.site.SiteInfo[] = [] + origin.forEach(host => originSiteInfo.push({ host })) + merged.forEach(host => originSiteInfo.push({ host, merged: true })) + virtual.forEach(host => originSiteInfo.push({ host, virtual: true })) + originSiteInfo.forEach(o => { + const key = calcUniqueKey(o) + !options[key] && (options[key] = o) + }) + trendDomainOptions.value = Object.values(options) searching.value = false } @@ -55,16 +68,21 @@ function hostInfoOfKey(key: string): timer.site.SiteKey { const MERGED_TAG_TXT = t(msg => msg.analysis.common.merged) const VIRTUAL_TAG_TXT = t(msg => msg.analysis.common.virtual) -function renderHostLabel(hostInfo: timer.site.SiteKey): VNode[] { + +const renderOptionTag = (tagLabel: string) => h('span', + { style: { float: "right", height: "34px" } }, + h(ElTag, { size: 'small' }, () => tagLabel) +) + +function renderHostLabel({ host, merged, virtual, alias }: timer.site.SiteInfo): VNode[] { const result = [ - h('span', {}, hostInfo.host) + h('span', {}, host) ] - hostInfo.merged && result.push( - h(ElTag, { size: 'small' }, () => MERGED_TAG_TXT) - ) - hostInfo.virtual && result.push( - h(ElTag, { size: 'small' }, () => VIRTUAL_TAG_TXT) + alias && result.push( + h(ElTag, { size: 'small', type: 'info' }, () => alias) ) + merged && result.push(renderOptionTag(MERGED_TAG_TXT)) + virtual && result.push(renderOptionTag(VIRTUAL_TAG_TXT)) return result } @@ -113,10 +131,10 @@ const _default = defineComponent({ handleSiteChange() } }, () => (trendDomainOptions.value || [])?.map( - hostInfo => h(ElOption, { - value: keyOfHostInfo(hostInfo), - label: labelOfHostInfo(hostInfo), - }, () => renderHostLabel(hostInfo)) + siteInfo => h(ElOption, { + value: keyOfHostInfo(siteInfo), + label: labelOfHostInfo(siteInfo), + }, () => renderHostLabel(siteInfo)) )), h(SelectFilterItem, { historyName: 'timeFormat', diff --git a/src/app/components/analysis/components/summary/index.ts b/src/app/components/analysis/components/summary/index.ts index 6ad21f20c..d47d9ae2e 100644 --- a/src/app/components/analysis/components/summary/index.ts +++ b/src/app/components/analysis/components/summary/index.ts @@ -28,7 +28,7 @@ function computeSummary(site: timer.site.SiteKey, rows: timer.stat.Row[]): Summa const summary: Summary = { focus: 0, visit: 0, day: 0 } summary.firstDay = rows?.[0]?.date - rows.forEach(({ focus, time: visit }) => { + rows?.forEach(({ focus, time: visit }) => { summary.focus += focus summary.visit += visit focus && (summary.day += 1) diff --git a/src/app/components/analysis/components/summary/summary.sass b/src/app/components/analysis/components/summary/summary.sass index 238b3a0b3..db9ea8be1 100644 --- a/src/app/components/analysis/components/summary/summary.sass +++ b/src/app/components/analysis/components/summary/summary.sass @@ -1,6 +1,7 @@ .analysis-summary-container height: 140px + margin-bottom: 0px !important >.el-col:not(:first-child) border-left: 1px var(--el-border-color) var(--el-border-style) .site-container diff --git a/src/app/components/analysis/components/trend/filter.ts b/src/app/components/analysis/components/trend/filter.ts index ceb940422..518ae924c 100644 --- a/src/app/components/analysis/components/trend/filter.ts +++ b/src/app/components/analysis/components/trend/filter.ts @@ -7,22 +7,22 @@ import type { ElementDatePickerShortcut } from "@src/element-ui/date" import type { PropType, Ref } from "vue" +import type { CalendarMessage } from "@i18n/message/common/calendar" import { t } from "@app/locale" -import { AnalysisMessage } from "@i18n/message/app/analysis" import { ElDatePicker } from "element-plus" import { defineComponent, h, ref } from "vue" import { daysAgo } from "@util/time" -function datePickerShortcut(msgKey: keyof AnalysisMessage['trend'], agoOfStart?: number, agoOfEnd?: number): ElementDatePickerShortcut { +function datePickerShortcut(msgKey: keyof CalendarMessage['range'], agoOfStart?: number, agoOfEnd?: number): ElementDatePickerShortcut { return { - text: t(msg => msg.analysis.trend[msgKey]), + text: t(msg => msg.calendar.range[msgKey]), value: daysAgo(agoOfStart - 1 || 0, agoOfEnd || 0) } } const SHORTCUTS = [ - datePickerShortcut('lastWeek', 7), + datePickerShortcut('last7Days', 7), datePickerShortcut('last15Days', 15), datePickerShortcut('last30Days', 30), datePickerShortcut("last90Days", 90) diff --git a/src/app/components/common/content-container.ts b/src/app/components/common/content-container.ts index 1a255a98a..cf2938280 100644 --- a/src/app/components/common/content-container.ts +++ b/src/app/components/common/content-container.ts @@ -1,11 +1,11 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import { ElCard } from "element-plus" +import { ElCard, ElScrollbar } from "element-plus" import ContentCard from "./content-card" import { defineComponent, h, useSlots } from "vue" @@ -19,7 +19,7 @@ const _default = defineComponent(() => { } else { content && children.push(h(ContentCard, () => h(content))) } - return () => h("div", { class: "content-container" }, children) + return () => h(ElScrollbar, () => h("div", { class: "content-container" }, children)) }) export default _default \ No newline at end of file diff --git a/src/app/components/dashboard/row1.ts b/src/app/components/dashboard/row1.ts index 697f5362e..29613a19e 100644 --- a/src/app/components/dashboard/row1.ts +++ b/src/app/components/dashboard/row1.ts @@ -15,8 +15,8 @@ import TopKVisit from './components/top-k-visit' const _default = defineComponent({ name: "DashboardRow1", render: () => h(ElRow, { - gutter: 40, - style: { height: '290px' } + gutter: 20, + style: { height: '300px' } }, () => [ h(DashboardCard, { span: 4 diff --git a/src/app/components/dashboard/style/index.sass b/src/app/components/dashboard/style/index.sass index d7d3340a7..cd0d06851 100644 --- a/src/app/components/dashboard/style/index.sass +++ b/src/app/components/dashboard/style/index.sass @@ -6,11 +6,9 @@ */ .el-row - margin-bottom: 50px + margin-bottom: 15px .el-col height: 100% - &:last-child - margin-bottom: 0 .el-card__body padding: 20px diff --git a/src/app/components/data-manage/clear/filter/operation-button.ts b/src/app/components/data-manage/clear/filter/operation-button.ts index d96b1bf24..aeb20b9f9 100644 --- a/src/app/components/data-manage/clear/filter/operation-button.ts +++ b/src/app/components/data-manage/clear/filter/operation-button.ts @@ -111,8 +111,8 @@ function generateParamAndSelect(props: _Props): Promise | unde return statDatabase.select(condition) } -const operationCancelMsg = t(msg => msg.dataManage.operationCancel) -const operationConfirmMsg = t(msg => msg.dataManage.operationConfirm) +const operationCancelMsg = t(msg => msg.button.cancel) +const operationConfirmMsg = t(msg => msg.button.confirm) const handleClick = async (props: _Props) => { const result: timer.stat.Row[] = await generateParamAndSelect(props) diff --git a/src/app/components/data-manage/clear/index.ts b/src/app/components/data-manage/clear/index.ts index 40720fdb1..a06784ded 100644 --- a/src/app/components/data-manage/clear/index.ts +++ b/src/app/components/data-manage/clear/index.ts @@ -19,8 +19,8 @@ type _Emits = { const statDatabase = new StatDatabase(chrome.storage.local) -const operationCancelMsg = t(msg => msg.dataManage.operationCancel) -const operationConfirmMsg = t(msg => msg.dataManage.operationConfirm) +const operationCancelMsg = t(msg => msg.button.cancel) +const operationConfirmMsg = t(msg => msg.button.confirm) async function handleClick(filterRef: Ref, ctx: SetupContext<_Emits>) { const filterOption: DataManageClearFilterOption = filterRef?.value?.getFilterOption() diff --git a/src/app/components/habit/component/filter.ts b/src/app/components/habit/component/filter.ts index 889ab5d5b..83abc13a9 100644 --- a/src/app/components/habit/component/filter.ts +++ b/src/app/components/habit/component/filter.ts @@ -5,10 +5,11 @@ * https://opensource.org/licenses/MIT */ -import type { PropType } from "vue" +import type { Ref, PropType } from "vue" import type { HabitMessage } from "@i18n/message/app/habit" +import type { CalendarMessage } from "@i18n/message/common/calendar" -import { ref, Ref, h, defineComponent } from "vue" +import { ref, h, defineComponent } from "vue" import { daysAgo } from "@util/time" import { t } from "@app/locale" import SwitchFilterItem from "@app/components/common/switch-filter-item" @@ -16,20 +17,20 @@ import { ElementDatePickerShortcut } from "@src/element-ui/date" import DateRangeFilterItem from "@app/components/common/date-range-filter-item" import SelectFilterItem from "@app/components/common/select-filter-item" -type ShortCutProp = [label: keyof HabitMessage['dateRange'], dayAgo: number] +type ShortCutProp = [label: keyof CalendarMessage['range'], dayAgo: number] const shortcutProps: ShortCutProp[] = [ - ["lastDay", 1], + ["last24Hours", 1], ["last3Days", 3], - ["lastWeek", 7], + ["last7Days", 7], ["last15Days", 15], ["last30Days", 30], ["last60Days", 60] ] -function datePickerShortcut(msg: keyof HabitMessage['dateRange'], agoOfStart: number): ElementDatePickerShortcut { +function datePickerShortcut(msg: keyof CalendarMessage['range'], agoOfStart: number): ElementDatePickerShortcut { return { - text: t(messages => messages.habit.dateRange[msg]), + text: t(messages => messages.calendar.range[msg]), value: daysAgo(agoOfStart, 0) } } diff --git a/src/app/components/help-us/index.ts b/src/app/components/help-us/index.ts index 3f85500de..600ac4fac 100644 --- a/src/app/components/help-us/index.ts +++ b/src/app/components/help-us/index.ts @@ -1,21 +1,22 @@ import { ElCard } from "element-plus" import { defineComponent, h } from "vue" import ContentContainer from "../common/content-container" -import HelpUsAlertInfo from "./alert-info" -import HelpUsToolbar from "./toolbar" -import HelpUsProgressList from "./progress-list" +import AlertInfo from "./alert-info" +import Toolbar from "./toolbar" +import ProgressList from "./progress-list" +import MemberList from "./member-list" import "./style" -const _default = defineComponent({ - name: "HelpUs", - render: () => h(ContentContainer, () => h(ElCard, +const _default = defineComponent(() => + () => h(ContentContainer, () => h(ElCard, { class: 'help-us' }, () => [ - h(HelpUsAlertInfo), - h(HelpUsToolbar), - h(HelpUsProgressList), + h(AlertInfo), + h(Toolbar), + h(ProgressList), + h(MemberList), ]) - ), -}) + ) +) export default _default \ No newline at end of file diff --git a/src/app/components/help-us/member-list.ts b/src/app/components/help-us/member-list.ts new file mode 100644 index 000000000..bdbcf544b --- /dev/null +++ b/src/app/components/help-us/member-list.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { getMembers, MemberInfo } from "@api/crowdin" +import { t } from "@app/locale" +import { ElDivider } from "element-plus" +import { defineComponent, h, onMounted, Ref, ref } from "vue" + +async function queryData(listRef: Ref) { + const list = await getMembers() || [] + list.sort((a, b) => (a.joinedAt || "").localeCompare(b.joinedAt || "")) + listRef.value = list +} + +function renderMember({ avatarUrl, username }: MemberInfo) { + const img = h('img', { src: avatarUrl, alt: username, title: username }) + const url = `https://crowdin.com/profile/${username}` + return h('a', { href: url, target: '_blank' }, img) +} + +const _default = defineComponent({ + name: 'HelpUsProgressList', + setup() { + const list: Ref = ref([]) + onMounted(() => queryData(list)) + return () => h('div', { class: 'member-container' }, [ + h(ElDivider, {}, () => t(msg => msg.helpUs.contributors)), + h('div', list.value.map(renderMember)) + ]) + }, +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/help-us/progress-list.ts b/src/app/components/help-us/progress-list.ts index a84dc1cbf..e1a53e2f6 100644 --- a/src/app/components/help-us/progress-list.ts +++ b/src/app/components/help-us/progress-list.ts @@ -22,8 +22,7 @@ const localeCrowdMap: { [locale in SupportedLocale]: string } = { es: "es-ES", ko: "ko", pl: "pl", - pt: "pt-PT", - pt_BR: "pt-BR", + pt_PT: "pt-PT", ru: "ru", uk: "uk", fr: "fr", diff --git a/src/app/components/help-us/style.sass b/src/app/components/help-us/style.sass index 29e070dc7..88d548163 100644 --- a/src/app/components/help-us/style.sass +++ b/src/app/components/help-us/style.sass @@ -3,6 +3,15 @@ margin: 30px 0 width: 100% display: flex + .member-container + margin-top: 10px + text-align: center + img + width: 60px + height: 60px + border-radius: 30px + a:not(:last-child) + margin-right: 15px .progress-container display: flex flex-direction: row diff --git a/src/app/components/limit/filter.ts b/src/app/components/limit/filter.ts index dd8ef2385..91edb34ee 100644 --- a/src/app/components/limit/filter.ts +++ b/src/app/components/limit/filter.ts @@ -5,17 +5,22 @@ * https://opensource.org/licenses/MIT */ -import { Operation, Plus } from "@element-plus/icons-vue" +import { Operation, Plus, SetUp } from "@element-plus/icons-vue" import { Ref, h, defineComponent, ref } from "vue" import InputFilterItem from "@app/components/common/input-filter-item" import SwitchFilterItem from "@app/components/common/switch-filter-item" import ButtonFilterItem from "@app/components/common/button-filter-item" import { t } from "@app/locale" +import { getAppPageUrl } from "@util/constant/url" +import { OPTION_ROUTE } from "@app/router/constants" +import { createTabAfterCurrent } from "@api/chrome/tab" const urlPlaceholder = t(msg => msg.limit.conditionFilter) const onlyEnabledLabel = t(msg => msg.limit.filterDisabled) -const addButtonText = t(msg => msg.limit.button.add) +const addButtonText = t(msg => msg.button.create) const testButtonText = t(msg => msg.limit.button.test) +const optionButtonText = t(msg => msg.limit.button.option) +const optionPageUrl = getAppPageUrl(false, OPTION_ROUTE, { i: 'dailyLimit' }) const emits = { create: () => true, @@ -60,6 +65,12 @@ const _default = defineComponent({ icon: Operation, onClick: () => ctx.emit('test') }), + h(ButtonFilterItem, { + text: optionButtonText, + icon: SetUp, + type: 'primary', + onClick: () => createTabAfterCurrent(optionPageUrl) + }), h(ButtonFilterItem, { text: addButtonText, type: "success", diff --git a/src/app/components/limit/modify/footer.ts b/src/app/components/limit/modify/footer.ts index 8e1f3a67e..be9744ee6 100644 --- a/src/app/components/limit/modify/footer.ts +++ b/src/app/components/limit/modify/footer.ts @@ -10,7 +10,7 @@ import { ElButton } from "element-plus" import { defineComponent, h } from "vue" import { t } from "@app/locale" -const buttonText = t(msg => msg.limit.button.save) +const buttonText = t(msg => msg.button.save) const _default = defineComponent({ name: "SaveButton", emits: { diff --git a/src/app/components/limit/modify/form/url.ts b/src/app/components/limit/modify/form/url.ts index 9522e3c9e..be5c7c0a7 100644 --- a/src/app/components/limit/modify/form/url.ts +++ b/src/app/components/limit/modify/form/url.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -12,6 +12,7 @@ import { ElButton, ElFormItem, ElInput, ElOption, ElSelect } from "element-plus" import { checkPermission, requestPermission } from "@api/chrome/permissions" import { IS_FIREFOX } from "@util/constant/environment" import { parseUrl } from "./common" +import { AUTHOR_EMAIL } from "@src/package" const ALL_PROTOCOLS: Protocol[] = ['http://', 'https://', '*://'] @@ -71,7 +72,7 @@ async function handlePaste(urlHandler: (newUrl: string) => void, protocolHandler } if (!granted) { - alert('Can\'t read the clipboard, please contact the developer via email to returnzhy1996@outlook.com') + alert(`Can\'t read the clipboard, please contact the developer via email to ${AUTHOR_EMAIL}`) return } @@ -81,7 +82,7 @@ async function handlePaste(urlHandler: (newUrl: string) => void, protocolHandler urlHandler?.(cleanUrl(url)) } -const pasteButtonText = t(msg => msg.limit.button.paste) +const pasteButtonText = t(msg => msg.button.paste) const placeholder = t(msg => msg.limit.urlPlaceholder) const _default = defineComponent({ diff --git a/src/app/components/limit/modify/style/el-input.sass b/src/app/components/limit/modify/style/el-input.sass index c17ef872a..b9d83e895 100644 --- a/src/app/components/limit/modify/style/el-input.sass +++ b/src/app/components/limit/modify/style/el-input.sass @@ -19,7 +19,7 @@ $prefixWidth: 110px // Fix shaking of clear button // @see https://github.com/sheepzh/timer/issues/123 position: absolute - right: 59px + right: 12px .el-input-group__append width: 12px .el-input__prefix diff --git a/src/app/components/limit/table/column/common.ts b/src/app/components/limit/table/column/common.ts new file mode 100644 index 000000000..b456c87c2 --- /dev/null +++ b/src/app/components/limit/table/column/common.ts @@ -0,0 +1,68 @@ +import { t, tN } from "@app/locale" +import { locale } from "@i18n" +import { VerificationPair } from "@service/limit-service/verification/common" +import verificationProcessor from "@service/limit-service/verification/processor" +import { ElMessageBox, ElMessage } from "element-plus" +import { h, VNode } from "vue" + +/** + * Judge wether verification is required + * + * @returns T/F + */ +export function judgeVerificationRequired(item: timer.limit.Item): boolean { + const { waste, time } = item || {} + return !!(waste > time * 1000) +} + +const PROMT_TXT_CSS: Partial = { + userSelect: 'none', +} + +/** + * @returns null if verification not required, + * or promise with resolve invocked only if verification code or password correct + */ +export async function processVerification(option: timer.option.DailyLimitOption): Promise { + const { limitLevel, limitPassword, limitVerifyDifficulty } = option + let answerValue: string + let messageNodes: (VNode | string)[] + let incrorectMessage: string + if (limitLevel === 'password' && limitPassword) { + answerValue = limitPassword + messageNodes = [t(msg => msg.limit.verification.pswInputTip)] + incrorectMessage = t(msg => msg.limit.verification.incorrectPsw) + } else if (limitLevel === 'verification') { + const pair: VerificationPair = verificationProcessor.generate(limitVerifyDifficulty, locale) + const { prompt, promptParam, answer } = pair || {} + answerValue = typeof answer === 'function' ? t(msg => answer(msg.limit.verification)) : answer + incrorectMessage = t(msg => msg.limit.verification.incorrectAnswer) + if (prompt) { + const promptTxt = typeof prompt === 'function' + ? t(msg => prompt(msg.limit.verification), { ...promptParam, answer: answerValue }) + : prompt + messageNodes = tN(msg => msg.limit.verification.inputTip, { prompt: h('b', promptTxt) }) + } else { + messageNodes = tN(msg => msg.limit.verification.inputTip2, { answer: h('b', answerValue) }) + } + } + return messageNodes?.length && answerValue + ? new Promise(resolve => + ElMessageBox({ + boxType: 'prompt', + type: 'warning', + title: '', + message: h('div', { style: PROMT_TXT_CSS }, messageNodes), + showInput: true, + showCancelButton: true, + showClose: true, + }).then(data => { + const { value } = data + if (value === answerValue) { + return resolve() + } + ElMessage.error(incrorectMessage) + }) + ) + : null +} diff --git a/src/app/components/limit/table/column/delay.ts b/src/app/components/limit/table/column/delay.ts index 616aa280c..49418b2db 100644 --- a/src/app/components/limit/table/column/delay.ts +++ b/src/app/components/limit/table/column/delay.ts @@ -9,10 +9,24 @@ import { InfoFilled } from "@element-plus/icons-vue" import { ElIcon, ElSwitch, ElTableColumn, ElTooltip } from "element-plus" import { defineComponent, h } from "vue" import { t } from "@app/locale" +import { judgeVerificationRequired, processVerification } from "./common" +import optionService from "@service/option-service" const label = t(msg => msg.limit.item.delayAllowed) const tooltip = t(msg => msg.limit.item.delayAllowedInfo) +async function handleChange(row: timer.limit.Item, newVal: boolean, callback: () => void) { + let promise: Promise = null + if (newVal && judgeVerificationRequired(row)) { + // Open delay for limited rules, so verification is required + const option = await optionService.getAllOption() + promise = processVerification(option) + } + promise + ? promise.then(callback).catch(() => { }) + : callback() +} + const _default = defineComponent({ name: "LimitDelayColumn", emits: { @@ -26,10 +40,10 @@ const _default = defineComponent({ }, { default: ({ row }: { row: timer.limit.Item }) => h(ElSwitch, { modelValue: row.allowDelay, - onChange(val: boolean) { + onChange: (val: boolean) => handleChange(row, val, () => { row.allowDelay = val ctx.emit("rowChange", row, val) - } + }) }), header: () => h('div', [ label, diff --git a/src/app/components/limit/table/column/enabled.ts b/src/app/components/limit/table/column/enabled.ts index 60b1d0ac9..6e690fd73 100644 --- a/src/app/components/limit/table/column/enabled.ts +++ b/src/app/components/limit/table/column/enabled.ts @@ -8,9 +8,23 @@ import { ElSwitch, ElTableColumn } from "element-plus" import { defineComponent, h } from "vue" import { t } from "@app/locale" +import { judgeVerificationRequired, processVerification } from "./common" +import optionService from "@service/option-service" const label = t(msg => msg.limit.item.enabled) +async function handleChange(row: timer.limit.Item, newVal: boolean, callback: () => void) { + let promise: Promise = null + if (!newVal && judgeVerificationRequired(row)) { + // Disable limited rules, so verification is required + const option = await optionService.getAllOption() + promise = processVerification(option) + } + promise + ? promise.then(callback).catch(() => { }) + : callback() +} + const _default = defineComponent({ name: "LimitEnabledColumn", emits: { @@ -25,10 +39,10 @@ const _default = defineComponent({ }, { default: ({ row }: { row: timer.limit.Item }) => h(ElSwitch, { modelValue: row.enabled, - onChange(val: boolean) { + onChange: (val: boolean) => handleChange(row, val, () => { row.enabled = val ctx.emit("rowChange", row, val) - } + }) }) }) } diff --git a/src/app/components/limit/table/column/operation.ts b/src/app/components/limit/table/column/operation.ts index a42e68094..d34a97121 100644 --- a/src/app/components/limit/table/column/operation.ts +++ b/src/app/components/limit/table/column/operation.ts @@ -9,12 +9,40 @@ import { Delete, Edit } from "@element-plus/icons-vue" import { ElButton, ElMessageBox, ElTableColumn } from "element-plus" import { defineComponent, h } from "vue" import { t } from "@app/locale" +import optionService from "@service/option-service" +import { judgeVerificationRequired, processVerification } from "./common" const label = t(msg => msg.limit.item.operation) -const deleteButtonText = t(msg => msg.limit.button.delete) -const modifyButtonText = t(msg => msg.limit.button.modify) +const deleteButtonText = t(msg => msg.button.delete) +const modifyButtonText = t(msg => msg.button.modify) + +async function handleDelete(row: timer.limit.Item, callback: () => void) { + let promise = undefined + if (judgeVerificationRequired(row)) { + const option = await optionService.getAllOption() as timer.option.DailyLimitOption + promise = processVerification(option) + } + if (!promise) { + const message = t(msg => msg.limit.message.deleteConfirm, { cond: row.cond }) + promise = ElMessageBox.confirm(message, { type: 'warning' }) + } + promise.then(callback).catch(() => { /** Do nothing */ }) +} + +async function handleModify(row: timer.limit.Item, callback: () => void) { + let promise: Promise = undefined + if (judgeVerificationRequired(row)) { + const option = await optionService.getAllOption() as timer.option.DailyLimitOption + promise = processVerification(option) + promise + ? promise.then(callback).catch(() => { }) + : callback() + } else { + callback() + } +} + const _default = defineComponent({ - name: "LimitOperationColumn", emits: { rowDelete: (_row: timer.limit.Item, _cond: string) => true, rowModify: (_row: timer.limit.Item) => true, @@ -31,19 +59,13 @@ const _default = defineComponent({ type: 'danger', size: 'small', icon: Delete, - onClick() { - const { cond } = row - const message = t(msg => msg.limit.message.deleteConfirm, { cond }) - ElMessageBox.confirm(message, { type: 'warning' }) - .then(() => ctx.emit("rowDelete", row, cond)) - .catch(() => { /** Do nothing */ }) - } + onClick: () => handleDelete(row, () => ctx.emit("rowDelete", row, row.cond)) }, () => deleteButtonText), h(ElButton, { type: 'primary', size: 'small', icon: Edit, - onClick: () => ctx.emit('rowModify', row), + onClick: () => handleModify(row, () => ctx.emit('rowModify', row)), }, () => modifyButtonText) ] }) diff --git a/src/app/components/limit/table/index.ts b/src/app/components/limit/table/index.ts index 07f4bd181..ff55803a1 100644 --- a/src/app/components/limit/table/index.ts +++ b/src/app/components/limit/table/index.ts @@ -15,7 +15,6 @@ import LimitEnabledColumn from "./column/enabled" import LimitOperationColumn from "./column/operation" const _default = defineComponent({ - name: "LimitTable", props: { data: Array as PropType }, diff --git a/src/app/components/limit/test.ts b/src/app/components/limit/test.ts index 5b5f0295e..ced70be6a 100644 --- a/src/app/components/limit/test.ts +++ b/src/app/components/limit/test.ts @@ -20,7 +20,7 @@ function computeResultTitle(url: string, inputting: boolean, matchedCondition: s return t(msg => msg.limit.message.inputTestUrl) } if (inputting) { - return t(msg => msg.limit.message.clickTestButton, { buttonText: t(msg => msg.limit.button.testSimple) }) + return t(msg => msg.limit.message.clickTestButton, { buttonText: t(msg => msg.button.test) }) } if (!matchedCondition?.length) { return t(msg => msg.limit.message.noRuleMatched) @@ -45,58 +45,55 @@ function computeResultType(url: string, inputting: boolean, matchedCondition: st return matchedCondition?.length ? 'success' : 'warning' } -const _default = defineComponent({ - name: "LimitTest", - setup(_props, ctx) { - const urlRef: Ref = ref() - const matchedConditionRef: Ref = ref([]) - const visible: Ref = ref(false) - const urlInputtingRef: Ref = ref(true) - const resultTitleRef: ComputedRef = computed(() => computeResultTitle(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) - const resultTypeRef: ComputedRef<_ResultType> = computed(() => computeResultType(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) - const resultDescRef: ComputedRef = computed(() => computeResultDesc(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) +const _default = defineComponent((_props, ctx) => { + const urlRef: Ref = ref() + const matchedConditionRef: Ref = ref([]) + const visible: Ref = ref(false) + const urlInputtingRef: Ref = ref(true) + const resultTitleRef: ComputedRef = computed(() => computeResultTitle(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) + const resultTypeRef: ComputedRef<_ResultType> = computed(() => computeResultType(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) + const resultDescRef: ComputedRef = computed(() => computeResultDesc(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) - const changeInput = (newVal: string) => (urlInputtingRef.value = true) && (urlRef.value = newVal?.trim()) - const test = () => { - urlInputtingRef.value = false - handleTest(urlRef.value).then(matched => matchedConditionRef.value = matched) - } - - ctx.expose({ - show() { - urlRef.value = '' - visible.value = true - urlInputtingRef.value = true - matchedConditionRef.value = [] - } - }) - return () => h(ElDialog, { - title: t(msg => msg.limit.button.testSimple), - modelValue: visible.value, - closeOnClickModal: false, - onClose: () => visible.value = false - }, () => [ - h(ElFormItem, { - label: t(msg => msg.limit.testUrlLabel), - labelWidth: 120 - }, () => h(ElInput, { - modelValue: urlRef.value, - clearable: true, - onClear: () => changeInput(''), - onKeyup: (event: KeyboardEvent) => event.key === 'Enter' && test(), - onInput: (newVal: string) => changeInput(newVal) - }, { - append: () => h(ElButton, { - onClick: () => test() - }, () => t(msg => msg.limit.button.testSimple)), - })), - h(ElAlert, { - closable: false, - type: resultTypeRef.value, - title: resultTitleRef.value, - }, () => resultDescRef.value.map(desc => h('li', desc))) - ]) + const changeInput = (newVal: string) => (urlInputtingRef.value = true) && (urlRef.value = newVal?.trim()) + const test = () => { + urlInputtingRef.value = false + handleTest(urlRef.value).then(matched => matchedConditionRef.value = matched) } + + ctx.expose({ + show() { + urlRef.value = '' + visible.value = true + urlInputtingRef.value = true + matchedConditionRef.value = [] + } + }) + return () => h(ElDialog, { + title: t(msg => msg.button.test), + modelValue: visible.value, + closeOnClickModal: false, + onClose: () => visible.value = false + }, () => [ + h(ElFormItem, { + label: t(msg => msg.limit.button.test), + labelWidth: 120 + }, () => h(ElInput, { + modelValue: urlRef.value, + clearable: true, + onClear: () => changeInput(''), + onKeyup: (event: KeyboardEvent) => event.key === 'Enter' && test(), + onInput: (newVal: string) => changeInput(newVal) + }, { + append: () => h(ElButton, { + onClick: () => test() + }, () => t(msg => msg.button.test)), + })), + h(ElAlert, { + closable: false, + type: resultTypeRef.value, + title: resultTitleRef.value, + }, () => resultDescRef.value.map(desc => h('li', desc))) + ]) }) export default _default \ No newline at end of file diff --git a/src/app/components/option/components/appearance/index.ts b/src/app/components/option/components/appearance/index.ts index b97b9d672..a9b468116 100644 --- a/src/app/components/option/components/appearance/index.ts +++ b/src/app/components/option/components/appearance/index.ts @@ -15,7 +15,7 @@ import DarkModeInput from "./dark-mode-input" import { t, tWith } from "@app/locale" import { renderOptionItem, tagText } from "../../common" import localeMessages from "@i18n/message/common/locale" -import { localeSameAsBrowser } from "@i18n" +import { ALL_LOCALES, localeSameAsBrowser } from "@i18n" import { toggle } from "@util/dark-mode" const displayWhitelist = (option: UnwrapRef) => h(ElSwitch, { @@ -42,10 +42,10 @@ const printInConsole = (option: UnwrapRef) => h(E } }) -const allLocales: timer.Locale[] = (["zh_CN", "zh_TW", "en", "ja"] as timer.Locale[]) +const SORTED_LOCALES: timer.Locale[] = ALL_LOCALES // Keep the locale same as this browser first position .sort((a, _b) => a === localeSameAsBrowser ? -1 : 0) -const allLocaleOptions: timer.option.LocaleOption[] = ["default", ...allLocales] +const allLocaleOptions: timer.option.LocaleOption[] = ["default", ...SORTED_LOCALES] const locale = (option: UnwrapRef) => h(ElSelect, { modelValue: option.locale, @@ -80,24 +80,6 @@ const locale = (option: UnwrapRef) => h(ElSelect, ) }) -const ALL_LIMIT_FILTER_TYPE: timer.limit.FilterType[] = [ - 'translucent', - 'groundGlass', -] - -const limitFilterTypeSelect = (option: timer.option.AppearanceOption) => h(ElSelect, { - modelValue: option.limitMarkFilter, - size: 'small', - onChange: (val: timer.limit.FilterType) => { - option.limitMarkFilter = val - optionService.setAppearanceOption(unref(option)) - } -}, { - default: () => ALL_LIMIT_FILTER_TYPE.map(item => - h(ElOption, { value: item, label: t(msg => msg.option.appearance.limitFilterType[item]) }) - ) -}) - function copy(target: timer.option.AppearanceOption, source: timer.option.AppearanceOption) { target.displayWhitelistMenu = source.displayWhitelistMenu target.displayBadgeText = source.displayBadgeText @@ -109,69 +91,59 @@ function copy(target: timer.option.AppearanceOption, source: timer.option.Appear target.limitMarkFilter = source.limitMarkFilter } -const _default = defineComponent({ - name: "AppearanceOptionContainer", - setup(_props, ctx) { - const option: UnwrapRef = reactive(defaultAppearance()) - optionService.getAllOption().then(currentVal => copy(option, currentVal)) - ctx.expose({ - async reset() { - copy(option, defaultAppearance()) - await optionService.setAppearanceOption(unref(option)) - toggle(await optionService.isDarkMode(option)) - } - }) - return () => h('div', [ - renderOptionItem({ - input: h(DarkModeInput, { - modelValue: option.darkMode, - startSecond: option.darkModeTimeStart, - endSecond: option.darkModeTimeEnd, - onChange: async (darkMode, range) => { - option.darkMode = darkMode - option.darkModeTimeStart = range?.[0] - option.darkModeTimeEnd = range?.[1] - await optionService.setAppearanceOption(unref(option)) - toggle(await optionService.isDarkMode()) - } - }) - }, - msg => msg.appearance.darkMode.label, - t(msg => msg.option.appearance.darkMode.options.default)), - h(ElDivider), - renderOptionItem({ - input: locale(option) - }, - msg => msg.appearance.locale.label, - t(msg => msg.option.appearance.locale.default) - ), - h(ElDivider), - renderOptionItem({ - input: displayWhitelist(option), - whitelist: tagText(msg => msg.option.appearance.whitelistItem), - contextMenu: tagText(msg => msg.option.appearance.contextMenu) - }, msg => msg.appearance.displayWhitelist, t(msg => msg.option.yes)), - h(ElDivider), - renderOptionItem({ - input: displayBadgeText(option), - timeInfo: tagText(msg => msg.option.appearance.badgeTextContent), - icon: tagText(msg => msg.option.appearance.icon) - }, msg => msg.appearance.displayBadgeText, t(msg => msg.option.yes)), - h(ElDivider), - renderOptionItem({ - input: printInConsole(option), - console: tagText(msg => msg.option.appearance.printInConsole.console), - info: tagText(msg => msg.option.appearance.printInConsole.info) - }, msg => msg.appearance.printInConsole.label, t(msg => msg.option.yes)), - h(ElDivider), - renderOptionItem({ - input: limitFilterTypeSelect(option) - }, - msg => msg.appearance.limitFilterType.label, - t(msg => msg.option.appearance.limitFilterType[defaultAppearance().limitMarkFilter]) - ) - ]) - } +const _default = defineComponent((_props, ctx) => { + const option: UnwrapRef = reactive(defaultAppearance()) + optionService.getAllOption().then(currentVal => copy(option, currentVal)) + ctx.expose({ + async reset() { + copy(option, defaultAppearance()) + await optionService.setAppearanceOption(unref(option)) + toggle(await optionService.isDarkMode(option)) + } + }) + return () => h('div', [ + renderOptionItem({ + input: h(DarkModeInput, { + modelValue: option.darkMode, + startSecond: option.darkModeTimeStart, + endSecond: option.darkModeTimeEnd, + onChange: async (darkMode, range) => { + option.darkMode = darkMode + option.darkModeTimeStart = range?.[0] + option.darkModeTimeEnd = range?.[1] + await optionService.setAppearanceOption(unref(option)) + toggle(await optionService.isDarkMode()) + } + }) + }, + msg => msg.appearance.darkMode.label, + t(msg => msg.option.appearance.darkMode.options.default)), + h(ElDivider), + renderOptionItem({ + input: locale(option) + }, + msg => msg.appearance.locale.label, + t(msg => msg.option.appearance.locale.default) + ), + h(ElDivider), + renderOptionItem({ + input: displayWhitelist(option), + whitelist: tagText(msg => msg.option.appearance.whitelistItem), + contextMenu: tagText(msg => msg.option.appearance.contextMenu) + }, msg => msg.appearance.displayWhitelist, t(msg => msg.option.yes)), + h(ElDivider), + renderOptionItem({ + input: displayBadgeText(option), + timeInfo: tagText(msg => msg.option.appearance.badgeTextContent), + icon: tagText(msg => msg.option.appearance.icon) + }, msg => msg.appearance.displayBadgeText, t(msg => msg.option.yes)), + h(ElDivider), + renderOptionItem({ + input: printInConsole(option), + console: tagText(msg => msg.option.appearance.printInConsole.console), + info: tagText(msg => msg.option.appearance.printInConsole.info) + }, msg => msg.appearance.printInConsole.label, t(msg => msg.option.yes)), + ]) }) export default _default \ No newline at end of file diff --git a/src/app/components/option/components/backup/footer.ts b/src/app/components/option/components/backup/footer.ts index 24b300a10..fee152c5b 100644 --- a/src/app/components/option/components/backup/footer.ts +++ b/src/app/components/option/components/backup/footer.ts @@ -4,12 +4,12 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import { PropType, Ref, watch } from "vue" +import type { PropType, Ref } from "vue" import { t } from "@app/locale" import { UploadFilled } from "@element-plus/icons-vue" import { ElButton, ElLoading, ElMessage, ElText } from "element-plus" -import { defineComponent, h, ref } from "vue" +import { defineComponent, h, ref, watch } from "vue" import metaService from "@service/meta-service" import processor from "@src/common/backup/processor" import { formatTime } from "@util/time" diff --git a/src/app/components/option/components/backup/index.ts b/src/app/components/option/components/backup/index.ts index 20b3b8a96..ec16f0c65 100644 --- a/src/app/components/option/components/backup/index.ts +++ b/src/app/components/option/components/backup/index.ts @@ -5,7 +5,8 @@ * https://opensource.org/licenses/MIT */ -import { Ref } from "vue" +import type { Ref } from "vue" +import type { I18nKey } from "@app/locale" import { t } from "@app/locale" import optionService from "@service/option-service" @@ -16,15 +17,26 @@ import { defineComponent, ref, h } from "vue" import { renderOptionItem, tooltip } from "../../common" import BackUpAutoInput from "./auto-input" import Footer from "./footer" +import { AUTHOR_EMAIL } from "@src/package" const ALL_TYPES: timer.backup.Type[] = [ 'none', 'gist', ] +const AUTH_LABELS: { [t in timer.backup.Type]: string } = { + 'none': '', + 'gist': 'Personal Access Token {info} {input}', +} + +const TYPE_NAMES: { [t in timer.backup.Type]: I18nKey } = { + none: msg => msg.option.backup.meta.none.label, + gist: _ => 'GitHub Gist', +} + const typeOptions = () => ALL_TYPES.map(type => h(ElOption, { value: type, - label: t(msg => msg.option.backup.meta[type].label) + label: t(TYPE_NAMES[type]), })) const typeSelect = (type: Ref, handleChange?: Function) => h(ElSelect, @@ -69,106 +81,103 @@ const authInput = (auth: Ref, handleInput: Function, handleTest: Functio const DEFAULT = defaultBackup() -const _default = defineComponent({ - name: "BackupOptionContainer", - setup(_props, ctx) { - const type: Ref = ref(DEFAULT.backupType) - const auth: Ref = ref('') - const clientName: Ref = ref(DEFAULT.clientName) - const autoBackUp: Ref = ref(DEFAULT.autoBackUp) - const autoBackUpInterval: Ref = ref(DEFAULT.autoBackUpInterval) - - optionService.getAllOption().then(currentVal => { - clientName.value = currentVal.clientName - type.value = currentVal.backupType - if (type.value) { - auth.value = currentVal.backupAuths?.[type.value] - } - autoBackUp.value = currentVal.autoBackUp - autoBackUpInterval.value = currentVal.autoBackUpInterval - }) +const _default = defineComponent((_props, ctx) => { + const type: Ref = ref(DEFAULT.backupType) + const auth: Ref = ref('') + const clientName: Ref = ref(DEFAULT.clientName) + const autoBackUp: Ref = ref(DEFAULT.autoBackUp) + const autoBackUpInterval: Ref = ref(DEFAULT.autoBackUpInterval) - function handleChange() { - const backupAuths = {} - backupAuths[type.value] = auth.value - const newOption: timer.option.BackupOption = { - backupType: type.value, - backupAuths, - clientName: clientName.value || DEFAULT.clientName, - autoBackUp: autoBackUp.value, - autoBackUpInterval: autoBackUpInterval.value, - } - optionService.setBackupOption(newOption) + optionService.getAllOption().then(currentVal => { + clientName.value = currentVal.clientName + type.value = currentVal.backupType + if (type.value) { + auth.value = currentVal.backupAuths?.[type.value] } + autoBackUp.value = currentVal.autoBackUp + autoBackUpInterval.value = currentVal.autoBackUpInterval + }) - async function handleTest() { - const loading = ElLoading.service({ - text: "Please wait...." - }) - const errorMsg = await processor.test(type.value, auth.value) - loading.close() - if (!errorMsg) { - ElMessage.success("Valid!") - } else { - ElMessage.error(errorMsg) - } + function handleChange() { + const backupAuths = {} + backupAuths[type.value] = auth.value + const newOption: timer.option.BackupOption = { + backupType: type.value, + backupAuths, + clientName: clientName.value || DEFAULT.clientName, + autoBackUp: autoBackUp.value, + autoBackUpInterval: autoBackUpInterval.value, } + optionService.setBackupOption(newOption) + } - ctx.expose({ - async reset() { - // Only reset type and auto flag - type.value = DEFAULT.backupType - autoBackUp.value = DEFAULT.autoBackUp - handleChange() - } + async function handleTest() { + const loading = ElLoading.service({ + text: "Please wait...." }) + const errorMsg = await processor.test(type.value, auth.value) + loading.close() + if (!errorMsg) { + ElMessage.success("Valid!") + } else { + ElMessage.error(errorMsg) + } + } - return () => { - const nodes = [ - h(ElAlert, { - closable: false, - type: "warning", - description: t(msg => msg.option.backup.alert) - }), - h(ElDivider), - renderOptionItem({ - input: typeSelect(type, handleChange) - }, - msg => msg.backup.type, - t(msg => msg.option.backup.meta[DEFAULT.backupType].label) - ) - ] - type.value !== 'none' && nodes.push( - h(ElDivider), - renderOptionItem({ - input: h(BackUpAutoInput, { - autoBackup: autoBackUp.value, - interval: autoBackUpInterval.value, - onChange(newAutoBackUp, newInterval) { - autoBackUp.value = newAutoBackUp - autoBackUpInterval.value = newInterval - handleChange() - } - }) - }, _msg => '{input}', t(msg => msg.option.no)), - h(ElDivider), - renderOptionItem({ - input: authInput(auth, handleChange, handleTest), - info: tooltip(msg => msg.option.backup.meta[type.value]?.authInfo) - }, - msg => msg.backup.meta[type.value]?.auth - ), - h(ElDivider), - renderOptionItem({ - input: clientNameInput(clientName, handleChange) - }, - msg => msg.backup.client - ), - h(ElDivider), - h(Footer, { type: type.value }), - ) - return h('div', nodes) + ctx.expose({ + async reset() { + // Only reset type and auto flag + type.value = DEFAULT.backupType + autoBackUp.value = DEFAULT.autoBackUp + handleChange() } + }) + + return () => { + const nodes = [ + h(ElAlert, { + closable: false, + type: "warning", + description: t(msg => msg.option.backup.alert, { email: AUTHOR_EMAIL }) + }), + h(ElDivider), + renderOptionItem({ + input: typeSelect(type, handleChange) + }, + msg => msg.backup.type, + t(TYPE_NAMES[DEFAULT.backupType]) + ) + ] + type.value !== 'none' && nodes.push( + h(ElDivider), + renderOptionItem({ + input: h(BackUpAutoInput, { + autoBackup: autoBackUp.value, + interval: autoBackUpInterval.value, + onChange(newAutoBackUp, newInterval) { + autoBackUp.value = newAutoBackUp + autoBackUpInterval.value = newInterval + handleChange() + } + }) + }, _msg => '{input}', t(msg => msg.option.no)), + h(ElDivider), + renderOptionItem({ + input: authInput(auth, handleChange, handleTest), + info: tooltip(msg => msg.option.backup.meta[type.value]?.authInfo) + }, + _msg => AUTH_LABELS[type.value], + ), + h(ElDivider), + renderOptionItem({ + input: clientNameInput(clientName, handleChange) + }, + msg => msg.backup.client + ), + h(ElDivider), + h(Footer, { type: type.value }), + ) + return h('div', nodes) } }) diff --git a/src/app/components/option/components/daily-limit/daily-limit.sass b/src/app/components/option/components/daily-limit/daily-limit.sass new file mode 100644 index 000000000..f31c2c536 --- /dev/null +++ b/src/app/components/option/components/daily-limit/daily-limit.sass @@ -0,0 +1,6 @@ +// Fallback with EN +.option-daily-limit-level-select>.select-trigger + width: 330px + +.option-daily-limit-level-select.zh_CN>.select-trigger + width: 210px diff --git a/src/app/components/option/components/daily-limit/index.ts b/src/app/components/option/components/daily-limit/index.ts new file mode 100644 index 000000000..a5448f2d1 --- /dev/null +++ b/src/app/components/option/components/daily-limit/index.ts @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@app/locale" +import { locale } from "@i18n" +import optionService from "@service/option-service" +import { defaultDailyLimit } from "@util/constant/option" +import { ElDivider, ElInput, ElOption, ElSelect } from "element-plus" +import { defineComponent, reactive, unref, UnwrapRef, h } from "vue" +import { renderOptionItem } from "../../common" +import "./daily-limit.sass" + +const ALL_LIMIT_FILTER_TYPE: timer.limit.FilterType[] = [ + 'translucent', + 'groundGlass', +] + +const ALL_LEVEL: timer.limit.RestrictionLevel[] = [ + 'nothing', + 'verification', + 'password', +] + +const ALL_DIFF: timer.limit.VerificationDifficulty[] = [ + 'easy', + 'hard', + 'disgusting', +] + +const filterSelect = (option: timer.option.DailyLimitOption) => h(ElSelect, { + modelValue: option.limitFilter, + size: 'small', + onChange: (val: timer.limit.FilterType) => { + option.limitFilter = val + optionService.setDailyLimitOption(unref(option)) + } +}, () => ALL_LIMIT_FILTER_TYPE.map(item => h(ElOption, { value: item, label: t(msg => msg.option.dailyLimit.filter[item]) }))) + +const levelSelect = (option: timer.option.DailyLimitOption) => h(ElSelect, { + modelValue: option.limitLevel, + size: 'small', + class: `option-daily-limit-level-select ${locale}`, + onChange: (val: timer.limit.RestrictionLevel) => { + option.limitLevel = val + optionService.setDailyLimitOption(unref(option)) + } +}, () => ALL_LEVEL.map(item => h(ElOption, { value: item, label: t(msg => msg.option.dailyLimit.level[item]) }))) + +const pswInput = (option: timer.option.DailyLimitOption) => h(ElInput, { + modelValue: option.limitPassword, + size: 'small', + type: 'password', + showPassword: true, + style: { width: '200px' }, + onInput: (val: string) => { + option.limitPassword = val?.trim() + optionService.setDailyLimitOption(unref(option)) + } +}) + +const veriDiffSelect = (option: timer.option.DailyLimitOption) => h(ElSelect, { + modelValue: option.limitVerifyDifficulty, + size: 'small', + onChange: (val: timer.limit.VerificationDifficulty) => { + option.limitVerifyDifficulty = val + optionService.setDailyLimitOption(unref(option)) + } +}, () => ALL_DIFF.map(item => h(ElOption, { value: item, label: t(msg => msg.option.dailyLimit.level.verificationDifficulty[item]) }))) + +function copy(target: timer.option.DailyLimitOption, source: timer.option.DailyLimitOption) { + target.limitFilter = source.limitFilter + target.limitLevel = source.limitLevel + target.limitPassword = source.limitPassword + target.limitVerifyDifficulty = source.limitVerifyDifficulty +} + +function reset(target: timer.option.DailyLimitOption) { + const defaultValue = defaultDailyLimit() + // Not to reset limitPassword + delete defaultValue.limitPassword + // Not to reset difficulty + delete defaultValue.limitVerifyDifficulty + Object.entries(defaultValue).forEach(([key, val]) => target[key] = val) +} + +const _default = defineComponent((_, ctx) => { + const option: UnwrapRef = reactive(defaultDailyLimit()) + optionService.getAllOption().then(currentVal => { + copy(option, currentVal) + }) + ctx.expose({ + reset: () => reset(option) + }) + return () => { + const nodes = [ + renderOptionItem({ + input: filterSelect(option) + }, + msg => msg.dailyLimit.filter.label, + t(msg => msg.option.dailyLimit.filter[defaultDailyLimit().limitFilter]) + ), + h(ElDivider), + renderOptionItem({ + input: levelSelect(option) + }, + msg => msg.dailyLimit.level.label, + t(msg => msg.option.dailyLimit.level[defaultDailyLimit().limitLevel]) + ), + ] + const { limitLevel } = option + limitLevel === 'password' && nodes.push( + h(ElDivider), + renderOptionItem({ + input: pswInput(option), + }, msg => msg.dailyLimit.level.passwordLabel) + ) + limitLevel === 'verification' && nodes.push( + h(ElDivider), + renderOptionItem({ + input: veriDiffSelect(option), + }, + msg => msg.dailyLimit.level.verificationLabel, + t(msg => msg.option.dailyLimit.level[defaultDailyLimit().limitVerifyDifficulty]) + ) + ) + return nodes + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/option/components/popup.ts b/src/app/components/option/components/popup.ts index 3887978ed..5f7bdcf94 100644 --- a/src/app/components/option/components/popup.ts +++ b/src/app/components/option/components/popup.ts @@ -9,6 +9,7 @@ import { unref, UnwrapRef } from "vue" import { ElDivider, ElInputNumber, ElOption, ElSelect, ElSwitch } from "element-plus" import { t } from "@app/locale" +import { I18nKey, t as t_ } from "@i18n" import { defineComponent, h, reactive } from "vue" import optionService from "@service/option-service" import { renderOptionItem, tagText } from "../common" @@ -18,6 +19,36 @@ import { ALL_DIMENSIONS } from "@util/stat" import { locale } from "@i18n" import { rotate } from "@util/array" +type LocaleStyle = { + durationSelectWidth: number + typeSelectWidth: number +} + +const STYLES: Messages = { + zh_CN: { + typeSelectWidth: 85, + durationSelectWidth: 80, + }, + en: { + typeSelectWidth: 115, + durationSelectWidth: 110 + }, + ja: { + typeSelectWidth: 85, + durationSelectWidth: 105, + }, + pt_PT: { + typeSelectWidth: 155, + durationSelectWidth: 120, + }, + zh_TW: { + typeSelectWidth: 85, + durationSelectWidth: 80, + }, +} + +const tStyle = (key: I18nKey) => t_(STYLES, { key }) + const mergeDomain = (option: UnwrapRef) => h(ElSwitch, { modelValue: option.defaultMergeDomain, onChange: (newVal: boolean) => { @@ -41,7 +72,7 @@ const typeOptions = () => ALL_DIMENSIONS.map(item => h(ElOption, { value: item, const typeSelect = (option: UnwrapRef) => h(ElSelect, { modelValue: option.defaultType, size: 'small', - style: { width: '120px' }, + style: { width: `${tStyle(m => m.typeSelectWidth)}px` }, onChange: (val: timer.stat.Dimension) => { option.defaultType = val optionService.setPopupOption(unref(option)) @@ -52,7 +83,7 @@ const durationOptions = () => ALL_POPUP_DURATION.map(item => h(ElOption, { value const durationSelect = (option: UnwrapRef) => h(ElSelect, { modelValue: option.defaultDuration, size: 'small', - style: { width: t(msg => msg.option.popup.durationWidth) }, + style: { width: `${tStyle(m => m.durationSelectWidth)}px` }, onChange: (val: PopupDuration) => { option.defaultDuration = val optionService.setPopupOption(unref(option)) diff --git a/src/app/components/option/index.ts b/src/app/components/option/index.ts index 675326ae7..207ec1089 100644 --- a/src/app/components/option/index.ts +++ b/src/app/components/option/index.ts @@ -14,6 +14,7 @@ import Popup from "./components/popup" import Appearance from "./components/appearance" import Statistics from "./components/statistics" import Backup from './components/backup' +import DailyLimit from './components/daily-limit' import './style' import { ElIcon, ElMessage, ElTabPane, ElTabs } from "element-plus" import { t } from "@app/locale" @@ -22,7 +23,7 @@ import { useRoute, useRouter } from "vue-router" const resetButtonName = "reset" const initialParamName = "i" -const allCategories = ["appearance", "statistics", "popup", 'backup'] as const +const allCategories = ["appearance", "statistics", "popup", 'dailyLimit', 'backup'] as const type _Category = typeof allCategories[number] function initWithQuery(tab: Ref<_Category>) { @@ -78,6 +79,7 @@ const _default = defineComponent({ statistics: ref(), popup: ref(), backup: ref(), + dailyLimit: ref(), } const router = useRouter() return () => h(ContentContainer, () => h(ElTabs, { @@ -106,7 +108,14 @@ const _default = defineComponent({ }, () => h(Popup, { ref: paneRefMap.popup })), - // backup + // Limit + h(ElTabPane, { + label: t(msg => msg.menu.limit), + name: "dailyLimit" as _Category + }, () => h(DailyLimit, { + ref: paneRefMap.dailyLimit + })), + // Backup h(ElTabPane, { label: t(msg => msg.option.backup.title), name: "backup" as _Category diff --git a/src/app/components/report/filter/index.ts b/src/app/components/report/filter/index.ts index 865f0f41b..6c973b771 100644 --- a/src/app/components/report/filter/index.ts +++ b/src/app/components/report/filter/index.ts @@ -7,7 +7,7 @@ import type { Ref, PropType } from "vue" import type { ElementDatePickerShortcut } from "@src/element-ui/date" -import type { ReportMessage } from "@i18n/message/app/report" +import type { CalendarMessage } from "@i18n/message/common/calendar" import DownloadFile from "./download-file" import RemoteClient from "./remote-client" @@ -34,11 +34,11 @@ const timeFormatLabels: { [key in timer.app.TimeFormat]: string } = { // Batch Delete const batchDeleteButtonText = t(msg => msg.report.batchDelete.buttonText) // Date range -const dateStartPlaceholder = t(msg => msg.report.startDate) -const dateEndPlaceholder = t(msg => msg.report.endDate) +const dateStartPlaceholder = t(msg => msg.calendar.label.startDate) +const dateEndPlaceholder = t(msg => msg.calendar.label.endDate) // date range -function datePickerShortcut(msg: keyof ReportMessage, agoOfStart?: number, agoOfEnd?: number): ElementDatePickerShortcut { - const text = t(messages => messages.report[msg]) +function datePickerShortcut(msg: keyof CalendarMessage['range'], agoOfStart?: number, agoOfEnd?: number): ElementDatePickerShortcut { + const text = t(messages => messages.calendar.range[msg]) const value = daysAgo(agoOfStart || 0, agoOfEnd || 0) return { text, value } } @@ -46,8 +46,9 @@ function datePickerShortcut(msg: keyof ReportMessage, agoOfStart?: number, agoOf const dateShortcuts: ElementDatePickerShortcut[] = [ datePickerShortcut('today'), datePickerShortcut('yesterday', 1, 1), - datePickerShortcut('lastWeek', 7), - datePickerShortcut('last30Days', 30) + datePickerShortcut('last7Days', 7), + datePickerShortcut('last30Days', 30), + datePickerShortcut('last60Days', 60), ] const _default = defineComponent({ diff --git a/src/app/components/report/table/columns/operation.ts b/src/app/components/report/table/columns/operation.ts index db456a8f7..758b4f01f 100644 --- a/src/app/components/report/table/columns/operation.ts +++ b/src/app/components/report/table/columns/operation.ts @@ -48,6 +48,7 @@ const LOCALE_WIDTH: { [locale in timer.Locale]: number } = { zh_CN: 290, ja: 360, zh_TW: 290, + pt_PT: 340, } const _default = defineComponent({ name: "OperationColumn", diff --git a/src/app/components/rule-merge/components/add-button.ts b/src/app/components/rule-merge/components/add-button.ts index 68cf25f15..d9345e50e 100644 --- a/src/app/components/rule-merge/components/add-button.ts +++ b/src/app/components/rule-merge/components/add-button.ts @@ -11,7 +11,7 @@ import { ElButton } from "element-plus" import { defineComponent, h, ref, Ref } from "vue" import ItemInput from './item-input' -const buttonText = `+ ${t(msg => msg.operation.newOne)}` +const buttonText = `+ ${t(msg => msg.button.create)}` const _default = defineComponent({ name: "MergeRuleAddButton", diff --git a/src/app/components/rule-merge/components/item.ts b/src/app/components/rule-merge/components/item.ts index 59de3d592..7238606a2 100644 --- a/src/app/components/rule-merge/components/item.ts +++ b/src/app/components/rule-merge/components/item.ts @@ -4,23 +4,16 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import type { MergeTagType } from "@util/merge" +import type { Ref } from "vue" import { t } from "@app/locale" import { Edit } from "@element-plus/icons-vue" import { tryParseInteger } from "@util/number" import { ElTag } from "element-plus" -import { computed, defineComponent, h, ref, Ref, watch } from "vue" +import { computed, defineComponent, h, ref, watch } from "vue" import ItemInput from "./item-input" - -function computeType(mergedVal: number | string): '' | 'info' | 'success' { - return typeof mergedVal === 'number' ? 'success' : mergedVal === '' ? 'info' : '' -} - -function computeTxt(mergedVal: number | string) { - return typeof mergedVal === 'number' - ? t(msg => msg.mergeRule.resultOfLevel, { level: mergedVal + 1 }) - : mergedVal === '' ? t(msg => msg.mergeRule.resultOfOrigin) : mergedVal -} +import { computeMergeTxt, computeMergeType } from "@util/merge" const _default = defineComponent({ name: "MergeRuleItem", @@ -47,8 +40,10 @@ const _default = defineComponent({ const id: Ref = ref(props.index || 0) watch(() => props.index, newVal => id.value = newVal) const editing: Ref = ref(false) - const type: Ref<'' | 'info' | 'success'> = computed(() => computeType(merged.value)) - const txt: Ref = computed(() => computeTxt(merged.value)) + const type: Ref = computed(() => computeMergeType(merged.value)) + const tagTxt: Ref = computed(() => computeMergeTxt(origin.value, merged.value, + (finder, param) => t(msg => finder(msg.mergeCommon), param) + )) ctx.expose({ forceEdit() { editing.value = true @@ -77,7 +72,10 @@ const _default = defineComponent({ type: type.value, closable: true, onClose: () => ctx.emit("delete", origin.value) - }, () => [`${origin.value} >>> ${txt.value}`, h(Edit, { class: "edit-icon", onclick: () => editing.value = true })]) + }, () => [ + tagTxt.value, + h(Edit, { class: "edit-icon", onclick: () => editing.value = true }) + ]) } }) diff --git a/src/app/components/whitelist/components/add-button.ts b/src/app/components/whitelist/components/add-button.ts index 6966c96c7..bca955295 100644 --- a/src/app/components/whitelist/components/add-button.ts +++ b/src/app/components/whitelist/components/add-button.ts @@ -10,7 +10,7 @@ import { ElButton } from "element-plus" import { defineComponent, h, ref, Ref } from "vue" import ItemInput from './item-input' -const buttonText = `+ ${t(msg => msg.operation.newOne)}` +const buttonText = `+ ${t(msg => msg.button.create)}` const _default = defineComponent({ name: "WhitelistAddButton", diff --git a/src/app/index.ts b/src/app/index.ts index 55b049925..e344738ef 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -24,7 +24,8 @@ const locales: { [locale in timer.Locale]: () => Promise<{ default: Language }> zh_CN: () => import('element-plus/lib/locale/lang/zh-cn'), zh_TW: () => import('element-plus/lib/locale/lang/zh-tw'), en: () => import('element-plus/lib/locale/lang/en'), - ja: () => import('element-plus/lib/locale/lang/ja') + ja: () => import('element-plus/lib/locale/lang/ja'), + pt_PT: () => import('element-plus/lib/locale/lang/pt'), } async function main() { diff --git a/src/app/layout/index.ts b/src/app/layout/index.ts index a9b54a5e4..ba9cc7bf9 100644 --- a/src/app/layout/index.ts +++ b/src/app/layout/index.ts @@ -1,25 +1,22 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import { ElAside, ElContainer } from "element-plus" +import { ElAside, ElContainer, ElScrollbar } from "element-plus" import { defineComponent, h } from "vue" import Menu from "./menu" import VersionTag from "./version-tag" import { RouterView } from "vue-router" -const _default = defineComponent({ - name: "Layout", - render() { - return h(ElContainer, {}, () => [ - h(ElAside, { width: `270px` }, () => h(Menu)), - h(ElContainer, { class: 'app-container' }, () => h(RouterView)), - h(VersionTag) - ]) - } +const _default = defineComponent(() => { + return () => h(ElContainer, {}, () => [ + h(ElAside, () => h(ElScrollbar, () => h(Menu))), + h(ElContainer, { class: 'app-container' }, () => h(RouterView)), + h(VersionTag) + ]) }) export default _default \ No newline at end of file diff --git a/src/app/layout/menu.ts b/src/app/layout/menu.ts index 624db8210..c97d5f4f3 100644 --- a/src/app/layout/menu.ts +++ b/src/app/layout/menu.ts @@ -1,26 +1,28 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import type { UnwrapRef } from "vue" +import type { Ref } from "vue" import type ElementIcon from "@src/element-ui/icon" -import type { RouteLocationNormalizedLoaded, Router } from "vue-router" +import type { MenuItemRegistered } from "element-plus" +import type { Router } from "vue-router" import type { I18nKey } from "@app/locale" import type { MenuMessage } from "@i18n/message/app/menu" -import { defineComponent, h, onMounted, reactive } from "vue" -import { ElIcon, ElMenu, ElMenuItem, ElMenuItemGroup, MenuItemRegistered } from "element-plus" -import { useRoute, useRouter } from "vue-router" +import { defineComponent, h, onMounted, ref, watch } from "vue" +import { ElIcon, ElMenu, ElMenuItem, ElMenuItemGroup } from "element-plus" +import { useRouter } from "vue-router" import { t } from "@app/locale" import { HOME_PAGE, FEEDBACK_QUESTIONNAIRE, getGuidePageUrl } from "@util/constant/url" import { Aim, Calendar, ChatSquare, Folder, HelpFilled, HotWater, Memo, Rank, SetUp, Stopwatch, Sugar, Tickets, Timer } from "@element-plus/icons-vue" import { locale } from "@i18n" import TrendIcon from "./icon/trend-icon" -import { createTab } from "@api/chrome/tab" -import { ANALYSIS_ROUTE } from "@app/router/constants" +import { createTabAfterCurrent } from "@api/chrome/tab" +import { ANALYSIS_ROUTE, MERGE_ROUTE } from "@app/router/constants" +import { START_ROUTE } from "@guide/router/constants" type _MenuItem = { title: keyof MenuMessage @@ -35,18 +37,13 @@ type _MenuGroup = { children: _MenuItem[] } -type _RouteProps = { - router: Router - current: RouteLocationNormalizedLoaded -} - /** * Generate menu items after locale initialized */ function generateMenus(): _MenuGroup[] { const otherMenuItems: _MenuItem[] = [{ title: 'userManual', - href: getGuidePageUrl(false), + href: getGuidePageUrl(false, START_ROUTE), icon: Memo, index: '_guide', }, { @@ -111,7 +108,7 @@ function generateMenus(): _MenuGroup[] { icon: Tickets }, { title: 'mergeRule', - route: '/additional/rule-merge', + route: MERGE_ROUTE, icon: Rank }, { title: 'option', @@ -124,23 +121,24 @@ function generateMenus(): _MenuGroup[] { }] } -function openMenu(route: string, title: I18nKey, routeProps: UnwrapRef<_RouteProps>) { - const routerVal = routeProps.router - const currentRouteVal = routeProps.current - if (currentRouteVal && currentRouteVal.path !== route) { - routerVal && routerVal.push(route) +function openMenu(route: string, title: I18nKey, router: Router) { + const currentPath = router.currentRoute.value?.path + if (currentPath !== route) { + router?.push(route) document.title = t(title) } } -const openHref = (href: string) => createTab(href) +const openHref = (href: string) => createTabAfterCurrent(href) -function handleClick(_MenuItem: _MenuItem, routeProps: UnwrapRef<_RouteProps>) { - const { route, title, href } = _MenuItem +function handleClick(menuItem: _MenuItem, router: Router, currentActive: Ref) { + const { route, title, href } = menuItem if (route) { - openMenu(route, msg => msg.menu[title], routeProps) + openMenu(route, msg => msg.menu[title], router) + currentActive.value = '/data/dashboard'//route } else { openHref(href) + currentActive.value = router.currentRoute?.value?.path } } @@ -151,10 +149,10 @@ const iconStyle: Partial = { lineHeight: '0.83em' } -function renderMenuLeaf(menu: _MenuItem, routeProps: UnwrapRef<_RouteProps>) { +function renderMenuLeaf(menu: _MenuItem, router: Router, currentActive: Ref) { const { route, title, icon, index } = menu const props: { onClick: (item: MenuItemRegistered) => void; index?: string } = { - onClick: (_item) => handleClick(menu, routeProps) + onClick: (_item) => handleClick(menu, router, currentActive) } const realIndex = index || route realIndex && (props.index = realIndex) @@ -164,9 +162,9 @@ function renderMenuLeaf(menu: _MenuItem, routeProps: UnwrapRef<_RouteProps>) { }) } -function renderMenu(menu: _MenuGroup, props: UnwrapRef<_RouteProps>) { +function renderMenu(menu: _MenuGroup, router: Router, currentActive: Ref) { const title = t(msg => msg.menu[menu.title]) - return h(ElMenuItemGroup, { title }, () => menu.children.map(item => renderMenuLeaf(item, props))) + return h(ElMenuItemGroup, { title }, () => menu.children.map(item => renderMenuLeaf(item, router, currentActive))) } async function initTitle(allMenus: _MenuGroup[], router: Router) { @@ -183,22 +181,22 @@ async function initTitle(allMenus: _MenuGroup[], router: Router) { } } -const _default = defineComponent({ - name: "LayoutMenu", - setup() { - const routeProps: UnwrapRef<_RouteProps> = reactive({ - router: useRouter(), - current: useRoute() - }) - - const allMenus = generateMenus() - onMounted(() => initTitle(allMenus, useRouter())) - - return () => h(ElMenu, - { defaultActive: routeProps.current.path }, - () => allMenus.map(menu => renderMenu(menu, routeProps)) - ) +const _default = defineComponent(() => { + const router = useRouter() + const currentActive: Ref = ref() + const syncRouter = () => { + const route = router.currentRoute.value + route && (currentActive.value = route.path) } + + watch(router.currentRoute, syncRouter) + + const allMenus = generateMenus() + onMounted(() => initTitle(allMenus, router)) + + return () => h('div', { class: 'menu-container' }, h(ElMenu, { defaultActive: currentActive.value }, + () => allMenus.map(menu => renderMenu(menu, router, currentActive)) + )) }) export default _default \ No newline at end of file diff --git a/src/app/router/constants.ts b/src/app/router/constants.ts index 242c15216..984beea02 100644 --- a/src/app/router/constants.ts +++ b/src/app/router/constants.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -18,4 +18,9 @@ export const LIMIT_ROUTE = '/behavior/limit' /** * @since 0.9.1 */ -export const REPORT_ROUTE = '/data/report' \ No newline at end of file +export const REPORT_ROUTE = '/data/report' + +/** + * @since 1.8.0 + */ +export const MERGE_ROUTE = '/additional/rule-merge' \ No newline at end of file diff --git a/src/app/router/index.ts b/src/app/router/index.ts index f11dd70e1..38fec026c 100644 --- a/src/app/router/index.ts +++ b/src/app/router/index.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -9,7 +9,7 @@ import type { App } from "vue" import type { RouteRecordRaw } from "vue-router" import { createRouter, createWebHashHistory } from "vue-router" -import { OPTION_ROUTE, ANALYSIS_ROUTE, LIMIT_ROUTE, REPORT_ROUTE } from "./constants" +import { OPTION_ROUTE, ANALYSIS_ROUTE, LIMIT_ROUTE, REPORT_ROUTE, MERGE_ROUTE } from "./constants" import metaService from "@service/meta-service" const dataRoutes: RouteRecordRaw[] = [ @@ -58,7 +58,7 @@ const additionalRoutes: RouteRecordRaw[] = [ path: '/additional/whitelist', component: () => import('../components/whitelist') }, { - path: '/additional/rule-merge', + path: MERGE_ROUTE, component: () => import('../components/rule-merge') }, { path: OPTION_ROUTE, diff --git a/src/app/styles/compatible.sass b/src/app/styles/compatible.sass index 58f1ad715..c11b71959 100644 --- a/src/app/styles/compatible.sass +++ b/src/app/styles/compatible.sass @@ -46,7 +46,8 @@ margin-left: 7px text-align: center font-size: 10px - +.el-divider__text + background-color: transparent \:root --el-input-height: 40px --el-input-inner-height: 38px diff --git a/src/app/styles/index.sass b/src/app/styles/index.sass index e5cce78d7..59c1455d4 100644 --- a/src/app/styles/index.sass +++ b/src/app/styles/index.sass @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -33,6 +33,11 @@ a padding-left: 45px !important .el-aside width: 240px + .el-menu + padding-bottom: 10px + .menu-container + min-height: 100vh + background-color: var(--el-menu-bg-color) .app-container width: 100% @@ -41,10 +46,11 @@ a .content-container margin-top: 30px - width: calc(100% - 60px) + margin-bottom: 30px + width: calc(100vw - 300px) height: calc(100% - 30px) padding: 0 30px - overflow-y: auto + overflow: hidden #app height: 100% diff --git a/src/background/browser-action-menu-manager.ts b/src/background/browser-action-menu-manager.ts index 7e288d679..f3b337148 100644 --- a/src/background/browser-action-menu-manager.ts +++ b/src/background/browser-action-menu-manager.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -13,6 +13,7 @@ import { createTab } from "@api/chrome/tab" import { createContextMenu } from "@api/chrome/context-menu" import { getRuntimeId } from "@api/chrome/runtime" import { locale } from "@i18n" +import { START_ROUTE } from "@guide/router/constants" const APP_PAGE_URL = getAppPageUrl(true) @@ -64,7 +65,7 @@ const feedbackPageProps: ChromeContextMenuCreateProps = { const guidePageProps: ChromeContextMenuCreateProps = { id: getRuntimeId() + '_timer_menu_item_guide_link', title: titleOf('📖', t2Chrome(msg => msg.base.guidePage)), - onclick: () => createTab(getGuidePageUrl(true)), + onclick: () => createTab(getGuidePageUrl(true, START_ROUTE)), ...baseProps } diff --git a/src/background/index.ts b/src/background/index.ts index bc23afe36..9b454e82a 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -22,7 +22,7 @@ import initCsHandler from "./content-script-handler" import { isBrowserUrl } from "@util/pattern" import BackupScheduler from "./backup-scheduler" import { createTab, listTabs } from "@api/chrome/tab" -import { isNoneWindowId, onNormalWindowFocusChanged } from "@api/chrome/window" +import { getWindow, isNoneWindowId, onNormalWindowFocusChanged } from "@api/chrome/window" import { onInstalled } from "@api/chrome/runtime" // Open the log of console @@ -62,14 +62,6 @@ new ActiveTabListener() .register(({ url, tabId }) => badgeTextManager.forceUpdate({ url, tabId })) .listen() -// Listen window focus changed -onNormalWindowFocusChanged(async windowId => { - if (isNoneWindowId(windowId)) return - const tabs = await listTabs({ windowId, active: true }) - tabs.filter(tab => !isBrowserUrl(tab?.url)) - .forEach(({ url, id }) => badgeTextManager.forceUpdate({ url, tabId: id })) -}) - // Collect the install time onInstalled(async reason => { if (reason === "install") { @@ -81,4 +73,14 @@ onInstalled(async reason => { }) // Start message dispatcher -messageDispatcher.start() \ No newline at end of file +messageDispatcher.start() + +// Listen window focus changed +onNormalWindowFocusChanged(async windowId => { + if (isNoneWindowId(windowId)) return + const window = await getWindow(windowId) + if (!window || window.type !== 'normal') return + const tabs = await listTabs({ windowId, active: true }) + tabs.filter(tab => !isBrowserUrl(tab?.url)) + .forEach(({ url, id }) => badgeTextManager.forceUpdate({ url, tabId: id })) +}) \ No newline at end of file diff --git a/src/content-script/locale.ts b/src/content-script/locale.ts index 2aa3a5302..8771f4da9 100644 --- a/src/content-script/locale.ts +++ b/src/content-script/locale.ts @@ -10,6 +10,6 @@ import type { ContentScriptMessage } from "@i18n/message/common/content-script" import { t as t_ } from "@i18n" import messages from "@i18n/message/common/content-script" -export function t(key: I18nKey): string { - return t_(messages, { key }) +export function t(key: I18nKey, param?: any): string { + return t_(messages, { key, param }) } \ No newline at end of file diff --git a/src/content-script/printer.ts b/src/content-script/printer.ts index 803ea1e02..55834a9ee 100644 --- a/src/content-script/printer.ts +++ b/src/content-script/printer.ts @@ -8,6 +8,7 @@ import { t } from "./locale" import { formatPeriod } from "@util/time" import { sendMsg2Runtime } from "@api/chrome/runtime" +import { t2Chrome } from "@i18n/chrome/t" /** * Print info of today @@ -24,7 +25,7 @@ export default async function printInfo(host: string) { .replace('{time}', waste.time ? '' + waste.time : '-') .replace('{focus}', formatPeriod(waste.focus, msg)) .replace('{host}', host) - const info1 = t(msg => msg.closeAlert) + const info1 = t(msg => msg.closeAlert).replace('{appName}', t2Chrome(msg => msg.meta.name)) console.log(info0) console.log(info1) } \ No newline at end of file diff --git a/src/database/site-database.ts b/src/database/site-database.ts index 4f4265c01..7fe50b2e1 100644 --- a/src/database/site-database.ts +++ b/src/database/site-database.ts @@ -11,6 +11,10 @@ import { REMAIN_WORD_PREFIX } from "./common/constant" export type SiteCondition = { host?: string alias?: string + /** + * Fuzzy query of host or alias + */ + fuzzyQuery?: string source?: timer.site.AliasSource virtual?: boolean } @@ -95,16 +99,18 @@ async function select(this: SiteDatabase, condition?: SiteCondition): Promise boolean { - const { host, alias, source, virtual } = condition || {} + const { host, alias, source, virtual, fuzzyQuery } = condition || {} return site => { - if (host && !site.host.includes(host)) return false - if (alias && !site.alias?.includes(alias)) return false - if (source && source !== site.source) return false + const { host: siteHost, alias: siteAlias, source: siteSource, virtual: siteVirtual } = site || {} + if (host && !siteHost.includes(host)) return false + if (alias && !siteAlias?.includes(alias)) return false + if (source && source !== siteSource) return false if (virtual !== undefined && virtual !== null) { const virtualCond = virtual || false - const virtualFactor = site.virtual || false + const virtualFactor = siteVirtual || false if (virtualCond !== virtualFactor) return false } + if (fuzzyQuery && !(siteHost?.includes(fuzzyQuery) || siteAlias?.includes(fuzzyQuery))) return false return true } } diff --git a/src/element-ui/table.ts b/src/element-ui/table.ts new file mode 100644 index 000000000..3b557895f --- /dev/null +++ b/src/element-ui/table.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { TableColumnCtx } from "element-plus" + +export type ElTableRowScope = { + row: T +} + +export type ElTableSpanMethodProps = { + row: T + column: TableColumnCtx + rowIndex: number + columnIndex: number +} \ No newline at end of file diff --git a/src/guide/component/app.ts b/src/guide/component/app.ts new file mode 100644 index 000000000..e1b960c99 --- /dev/null +++ b/src/guide/component/app.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { MERGE_ROUTE, PRIVACY_ROUTE } from "@guide/router/constants" +import { p, ul, alert } from "./common/util" +import { t } from "@guide/locale" + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: PRIVACY_ROUTE, + title: msg => msg.privacy.title, + }, + next: { + route: MERGE_ROUTE, + title: msg => msg.merge.title, + }, + title: msg => msg.app.title, + }, () => [ + p(msg => msg.app.p1), + ul( + [msg => msg.app.l1, { button: t(msg => msg.base.allFunction) }], + [msg => msg.app.l2, { button: t(msg => msg.base.allFunction) }], + ), + alert(msg => msg.app.p2, 'success'), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/backup.ts b/src/guide/component/backup.ts new file mode 100644 index 000000000..2da3eb28e --- /dev/null +++ b/src/guide/component/backup.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { LIMIT_ROUTE } from "@guide/router/constants" +import { p, ul, h2, alert, appLink, link } from "./common/util" +import { OPTION_ROUTE, REPORT_ROUTE } from "@app/router/constants" +import { t } from "@guide/locale" +import { ElButton } from "element-plus" +import { UploadFilled } from "@element-plus/icons-vue" + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: LIMIT_ROUTE, + title: msg => msg.limit.title + }, + title: msg => msg.backup.title, + }, () => [ + p(msg => msg.backup.p1, { link: link('https://gist.github.com') }), + h2(msg => msg.backup.upload.title), + ul( + [msg => msg.backup.upload.prepareToken, { link: link('https://github.com/settings/tokens') }], + [msg => msg.backup.upload.enter, { link: appLink(OPTION_ROUTE, { i: 'backup' }) }], + msg => msg.backup.upload.form, + msg => msg.backup.upload.backup, + ), + h2(msg => msg.backup.query.title), + p(msg => msg.backup.query.p1), + ul( + [msg => msg.backup.query.enter, { + link: appLink(REPORT_ROUTE), + menuItem: t(msg => msg.appMenu.dataReport), + }], [msg => msg.backup.query.enable, { + icon: h(ElButton, { + type: 'text', + link: true, + icon: UploadFilled, + disabled: true + }) + }], + msg => msg.backup.query.wait, + ), + alert(msg => msg.backup.query.tip, 'warning'), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/common.ts b/src/guide/component/common.ts deleted file mode 100644 index 33e4de794..000000000 --- a/src/guide/component/common.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { I18nKey } from "@guide/locale" -import type { VNode } from "vue" - -import { t, tN } from "@guide/locale" -import { h } from "vue" -import { position2AnchorClz } from "@guide/util" -import { createTab } from "@api/chrome/tab" - -export function h1(i18nKey: I18nKey, position: Position, i18nParam?: any): VNode { - return h('h1', { class: `guide-h1 ${position2AnchorClz(position)}` }, t(i18nKey, i18nParam)) -} - -export function h2(i18nKey: I18nKey, position: Position): VNode { - return h('h2', { class: `guide-h2 ${position2AnchorClz(position)}` }, t(i18nKey)) -} - -export function paragraph(i18nKey: I18nKey, param?: any): VNode { - return h('div', { class: 'guide-paragragh' }, tN(i18nKey, param)) -} - -export function link(href: string, text: string): VNode { - return h('a', { class: 'guide-link', href, target: "_blank" }, text) -} - -export function linkInner(extensionUrl: string, text: string): VNode { - return h('a', { - class: 'guide-link', - onClick: () => createTab(extensionUrl), - }, text) -} - -export function list(...items: (I18nKey | [I18nKey, any])[]): VNode { - const children = items.map(item => { - let param = undefined - let i18nKey: I18nKey = undefined - if (Array.isArray(item)) { - [i18nKey, param] = item - } else { - i18nKey = item - } - return h('li', { class: 'guide-list-item' }, tN(i18nKey, param)) - }) - return h('ul', { class: 'guide-list' }, children) -} - -export function section(...vnodes: VNode[]): VNode { - return h('section', { class: 'guide-area' }, vnodes) -} \ No newline at end of file diff --git a/src/guide/component/common/article.sass b/src/guide/component/common/article.sass new file mode 100644 index 000000000..85b5993dc --- /dev/null +++ b/src/guide/component/common/article.sass @@ -0,0 +1,72 @@ +$footerH: 60px +$titleColor: #222 +$titleColorDark: #DDD +.article-container + margin: 0 15% 50px 15% + font-size: 15px + line-height: 1.5em + font-family: Segoe UI,SegoeUI,Helvetica Neue,Helvetica,Arial,sans-serif + h1, h2 + color: var(--guide-article-title-color) + .article-title + margin-top: 0px + margin-bottom: 32px + font-weight: 600 + font-size: 40px + line-height: 1.2em + .article-content + padding-bottom: 60px + text-align: justify + p, ul + color: var(--el-text-color-primary) + font-size: 16px + line-height: 1.6 + letter-spacing: 0.03em + ul + margin: 16px 0 16px 38px + padding: 0px + li + list-style: disc + h2 + font-size: 28px + line-height: 2 + font-weight: 700 + margin: 1.2em 0 0.8em + .img-container + width: 100% + text-align: center + .el-alert + margin: 30px 0 + padding: 12px 16px + border-radius: 6pxs + .el-alert__icon.is-big + font-size: var(--el-alert-icon-size) + width: var(--el-alert-icon-size) + .el-table__cell + .el-button.is-link + padding: 0 + .article-footer + height: $footerH + display: block + width: 100% + border-top: var(--el-border) + color: var(--el-text-color-primary) + .previous-container,.next-container + height: 100% + display: inline-flex + box-sizing: content-box + font-size: 18px + line-height: $footerH + vertical-align: middle + cursor: pointer + svg + margin: auto 5px + height: 18px + width: 18px + .next-container + float: right + +// Dark Mode +html[data-theme='dark'] + .article-container img + filter: brightness(0.85) saturate(1.25) diff --git a/src/guide/component/common/article.ts b/src/guide/component/common/article.ts new file mode 100644 index 000000000..4d7c936d7 --- /dev/null +++ b/src/guide/component/common/article.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { I18nKey } from "@guide/locale" +import type { PropType } from "vue" +import type { Router } from "vue-router" + +import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue" +import { t } from "@guide/locale" +import { defineComponent, h } from "vue" +import { useRouter } from "vue-router" + +type Link = { + route: string + title: I18nKey +} + +function renderFooter(previous: Link, next: Link, router: Router) { + return h('div', { class: 'article-footer' }, [ + previous && h('div', { + class: 'previous-container', onClick: () => router.push(previous.route) + }, [ + h(ArrowLeft), + h('span', t(previous.title)), + ]), + next && h('div', { class: 'next-container', onClick: () => router.push(next.route) }, [ + h('span', t(next.title)), + h(ArrowRight), + ]) + ]) +} + +const _default = defineComponent({ + props: { + title: { + type: Function as PropType, + required: true, + }, + next: Object as PropType, + previous: Object as PropType, + }, + setup(props, ctx) { + const { previous, next, title } = props + const router = useRouter() + const content = ctx.slots.default + return () => h('div', { class: 'article-container' }, [ + h('h1', { class: 'article-title' }, t(title)), + content && h('div', { class: 'article-content' }, h(content)), + (previous || next) && renderFooter(previous, next, router) + ]) + } +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/common/util.ts b/src/guide/component/common/util.ts new file mode 100644 index 000000000..e02928d76 --- /dev/null +++ b/src/guide/component/common/util.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { I18nKey } from "@guide/locale" +import type { VNode } from "vue" + +import { getUrl } from "@api/chrome/runtime" +import { createTabAfterCurrent } from "@api/chrome/tab" +import { Link } from "@element-plus/icons-vue" +import { t, tN } from "@guide/locale" +import { getAppPageUrl } from "@util/constant/url" +import { ElAlert, ElButton } from "element-plus" +import { h } from "vue" + +/** + * paragraph + */ +export function p(i18nKey: I18nKey, param?: any): VNode { + return h('p', tN(i18nKey, param)) +} + +/** + * Subtitle + */ +export function h2(i18nKey: I18nKey, param?: any): VNode { + return h('h2', tN(i18nKey, param)) +} + +/** + * Image + */ +export function img(fileName: string, param?: { width?: number, height?: number }): VNode { + return h('div', { class: 'img-container' }, + h('img', { ...(param || {}), src: getUrl(`static/images/guide/${fileName}`) }) + ) +} + +/** + * Alert + */ +export function alert(i18nKey: I18nKey, type: 'success' | 'warning' | 'info' = 'info', param?: any): VNode { + return h(ElAlert, { + type, + closable: false, + showIcon: true, + }, () => t(i18nKey, param)) +} + +/** + * ul + */ +export function ul(...liKeys: ([I18nKey, any] | I18nKey)[]): VNode { + return h('ul', { class: 'list-container' }, + liKeys.map(liKey => { + let i18nKey: I18nKey = undefined, param = undefined + if (typeof liKey === 'function') { + i18nKey = liKey + } else { + i18nKey = liKey[0] + param = liKey[1] + } + return h('li', tN(i18nKey, param)) + }) + ) +} + +export const appLink = (route?: string, query?: any) => link(getAppPageUrl(false, route, query)) + +export const link = (url: string) => h(ElButton, { + link: true, + icon: Link, + type: 'primary', + onClick: () => createTabAfterCurrent(url) +}) \ No newline at end of file diff --git a/src/guide/component/home/download-button.ts b/src/guide/component/home/download-button.ts new file mode 100644 index 000000000..13e4bea5b --- /dev/null +++ b/src/guide/component/home/download-button.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { createTabAfterCurrent } from "@api/chrome/tab" +import { ArrowDown, Download } from "@element-plus/icons-vue" +import { t } from "@guide/locale" +import { IS_CHROME, IS_EDGE, IS_FIREFOX } from "@util/constant/environment" +import { CHROME_HOMEPAGE, EDGE_HOMEPAGE, FIREFOX_HOMEPAGE } from "@util/constant/url" +import { ElButton, ElDropdown, ElDropdownItem, ElDropdownMenu, ElIcon } from "element-plus" +import { defineComponent, h } from "vue" + +type _Info = { + name: string + url: string +} + +function allInfo(): _Info[] { + const result: _Info[] = [] + !IS_CHROME && result.push({ name: 'Chrome', url: CHROME_HOMEPAGE }) + !IS_EDGE && result.push({ name: 'Edge', url: EDGE_HOMEPAGE }) + !IS_FIREFOX && result.push({ name: 'Firefox', url: FIREFOX_HOMEPAGE }) + return result +} + +function openUrl(url: string) { + if (!url) return + createTabAfterCurrent(url) +} + +function i18nOfBtn(name: string) { + return t(msg => msg.home.download, { browser: name }) +} + +const _default = defineComponent(() => { + const infos = allInfo() + const firstInfo = infos[0] + const dropdownInfos = infos.slice(1) + return () => h('div', { class: 'download-container' }, [ + firstInfo && h(ElButton, { icon: Download, onClick: () => openUrl(firstInfo.url) }, () => i18nOfBtn(firstInfo.name)), + dropdownInfos?.length && h(ElDropdown, { + onCommand: openUrl, + trigger: 'hover' + }, { + default: () => h(ElIcon, {}, () => h(ArrowDown)), + dropdown: () => h(ElDropdownMenu, {}, () => dropdownInfos + .map(info => h(ElDropdownItem, { command: info.url }, () => i18nOfBtn(info.name))) + ) + }) + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/home/home.sass b/src/guide/component/home/home.sass new file mode 100644 index 000000000..89a867206 --- /dev/null +++ b/src/guide/component/home/home.sass @@ -0,0 +1,45 @@ +.home-container + margin-top: 50px + margin-bottom: 40px + text-align: center + .slogan + font-size: 48px + line-height: 1.25em + font-weight: 600 + margin-bottom: 0.5em + color: var(--el-text-color-primary) + .desc + font-size: 18px + line-height: 1.75em + margin-bottom: 24px + font-weight: 400 + color: var(--el-text-color-regular) + max-width: 55% + margin: 0 auto + padding-bottom: 24px + .button-container + margin-top: 20px + font-size: 16px + font-weight: 400px + .el-button + height: 46px + .download-container + margin-left: 30px + display: inline-block + .el-button + border-top-right-radius: 0 + border-bottom-right-radius: 0 + .el-dropdown + vertical-align: middle + height: 44px + border: var(--el-border) + border-left: none + border-top-right-radius: var(--el-border-radius-base) + border-bottom-right-radius: var(--el-border-radius-base) + background-color: var(--el-border-color) + .el-icon + margin: auto 2px + .el-icon:focus-visible + outline: none + img + width: 23% diff --git a/src/guide/component/home/index.ts b/src/guide/component/home/index.ts new file mode 100644 index 000000000..83a789d5e --- /dev/null +++ b/src/guide/component/home/index.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@guide/locale" +import { defineComponent, h } from "vue" +import DownloadButton from "./download-button" +import StartButton from "./start-button" +import './home.sass' + +const PIC_URL = chrome.runtime.getURL("static/images/guide/home.png") + +const _default = defineComponent(() => { + return () => h('div', { class: 'home-container' }, [ + h('h1', { class: 'slogan' }, t(msg => msg.meta.slogan)), + h('img', { src: PIC_URL }), + h('p', { class: 'desc' }, t(msg => msg.home.desc, { appName: t(msg => msg.meta.name) })), + h('div', { class: 'button-container' }, [ + h(StartButton), + h(DownloadButton) + ]), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/home/start-button.ts b/src/guide/component/home/start-button.ts new file mode 100644 index 000000000..95293b894 --- /dev/null +++ b/src/guide/component/home/start-button.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@guide/locale" +import { START_ROUTE } from "@guide/router/constants" +import { ElButton } from "element-plus" +import { defineComponent, h } from "vue" +import { useRouter } from "vue-router" +import { Document } from "@element-plus/icons-vue" + +const _default = defineComponent(() => { + const router = useRouter() + return () => h(ElButton, { + class: 'start-button', + type: 'primary', + onClick: () => router.push(START_ROUTE), + icon: Document, + }, () => t(msg => msg.home.button)) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/limit.ts b/src/guide/component/limit.ts new file mode 100644 index 000000000..ec4aeaa76 --- /dev/null +++ b/src/guide/component/limit.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { BACKUP_ROUTE, VIRTUAL_ROUTE } from "@guide/router/constants" +import { p, h2, ul, appLink } from "./common/util" +import { t } from "@guide/locale" + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: VIRTUAL_ROUTE, + title: msg => msg.virtual.title + }, + next: { + route: BACKUP_ROUTE, + title: msg => msg.backup.title, + }, + title: msg => msg.limit.title, + }, () => [ + p(msg => msg.limit.p1), + h2(msg => msg.limit.step.title), + ul( + [msg => msg.limit.step.enter, { link: appLink(), menuItem: t(msg => msg.appMenu.limit) }], + msg => msg.limit.step.click, + msg => msg.limit.step.form, + msg => msg.limit.step.check, + ), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/merge/index.ts b/src/guide/component/merge/index.ts new file mode 100644 index 000000000..cf53c963c --- /dev/null +++ b/src/guide/component/merge/index.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "../common/article" +import { APP_PAGE_ROUTE, VIRTUAL_ROUTE } from "@guide/router/constants" +import { p, h2, appLink, } from "../common/util" +import { MERGE_ROUTE } from "@app/router/constants" +import { renderTargetTable, renderSiteExampleTable } from "./target-table" +import { renderSourceTable } from "./source-table" +import { renderRuleTag } from "./rule-tag" +import "./merge.sass" + +const renderDemo = () => renderRuleTag('gist.github.com', 'github.com') + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: APP_PAGE_ROUTE, + title: msg => msg.app.title + }, + next: { + route: VIRTUAL_ROUTE, + title: msg => msg.virtual.title, + }, + title: msg => msg.merge.title, + }, () => [ + p(msg => msg.merge.p1, { + demo1: h('i', 'www.github.com'), + demo2: h('i', 'gist.github.com'), + }), + p(msg => msg.merge.p2, { link: appLink(MERGE_ROUTE) }), + h2(msg => msg.merge.lookTitle), + p(msg => msg.merge.p3, { demo: renderDemo() }), + h2(msg => msg.merge.source.title), + p(msg => msg.merge.source.p1), + renderSourceTable(), + h2(msg => msg.merge.target.title), + p(msg => msg.merge.target.p1), + renderTargetTable(), + p(msg => msg.merge.target.p2), + renderSiteExampleTable(), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/merge/merge.sass b/src/guide/component/merge/merge.sass new file mode 100644 index 000000000..954eabab7 --- /dev/null +++ b/src/guide/component/merge/merge.sass @@ -0,0 +1,8 @@ +.source-example-cell + display: flex + justify-content: space-between + word-wrap: break-word + .el-tag:last-child + margin-right: auto + .el-tag:not(last-child) + margin-right: 10px diff --git a/src/guide/component/merge/rule-tag.ts b/src/guide/component/merge/rule-tag.ts new file mode 100644 index 000000000..92923eb4e --- /dev/null +++ b/src/guide/component/merge/rule-tag.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@guide/locale" +import { computeMergeTxt, computeMergeType } from "@util/merge" +import { ElTag } from "element-plus" +import { h } from "vue" + +export const renderRuleTag = (origin: string, merged: string | number) => { + const mergedVal = merged ?? '' + return h(ElTag, { + type: computeMergeType(mergedVal), + size: 'small', + }, () => computeMergeTxt(origin, mergedVal, (finder, param) => t(msg => finder(msg.mergeCommon), param))) +} diff --git a/src/guide/component/merge/source-table.ts b/src/guide/component/merge/source-table.ts new file mode 100644 index 000000000..64ff6ca1f --- /dev/null +++ b/src/guide/component/merge/source-table.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { I18nKey } from "@guide/locale" + +import { t, tN } from "@guide/locale" +import { ElTableRowScope } from "@src/element-ui/table" +import { ElTable, ElTableColumn, ElTag } from "element-plus" +import { h } from "vue" + +type _SourceExample = { + source: string + examples: string[] | I18nKey +} + +const renderSiteExample = (site: string) => h(ElTag, { type: 'info', size: 'small' }, () => site) + +const SOURCE_EXAMPLES: _SourceExample[] = [{ + source: 'www.google.com', + examples: msg => msg.merge.source.only, +}, { + source: 'www.google.com.*', + examples: ['www.google.com.hk', 'www.google.com.au'], +}, { + source: '**.mit.edu', + examples: ['www.mit.edu', 'libraries.mit.edu', 'web.mit.edu', 'foo.bar.mit.edu'], +}] + +const renderSourceExample = (row: _SourceExample) => { + const { source, examples } = row + if (typeof examples === 'function') { + return h('span', tN(examples, { source: renderSiteExample(source) })) + } + const exampleTags = examples.map(renderSiteExample) + return h('div', { class: 'source-example-cell' }, exampleTags) +} + +export const renderSourceTable = () => h(ElTable, { + data: SOURCE_EXAMPLES, + border: true, + fit: true, +}, () => [ + h(ElTableColumn, { + label: t(msg => msg.merge.sourceCol), + width: 240, + }, { + default: ({ row }: ElTableRowScope<_SourceExample>) => row.source + }), + h(ElTableColumn, { label: t(msg => msg.merge.source.exampleCol) }, { + default: ({ row }: ElTableRowScope<_SourceExample>) => renderSourceExample(row) + }), +]) \ No newline at end of file diff --git a/src/guide/component/merge/target-table.ts b/src/guide/component/merge/target-table.ts new file mode 100644 index 000000000..de27b18d1 --- /dev/null +++ b/src/guide/component/merge/target-table.ts @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { ElTableRowScope } from "@src/element-ui/table" +import type { I18nKey } from "@guide/locale" +import type { VNode } from "vue" + +import { t, tN } from "@guide/locale" +import { ElTable, ElTableColumn } from "element-plus" +import { h } from 'vue' +import { renderRuleTag } from "./rule-tag" +import CustomizedHostMergeRuler from "@service/components/host-merge-ruler" +import { PSL_HOMEPAGE } from "@util/constant/url" +import { link } from "../common/util" + +type _RuleExample = { + source: string + target?: string | number +} + +type _TargetExample = _RuleExample & { + remark: I18nKey +} + +type _SiteExample = { + original: string + ruleIdx?: number | number[] + remark?: string | (VNode | string)[] +} + +function computeTarget(val: undefined | string | number): string | number { + return typeof val === 'number' ? val + 1 : (val || '') +} + +const renderTag = (rule: _RuleExample) => renderRuleTag(rule?.source, rule?.target) + +const TARGET_EXAMPLES: _TargetExample[] = [{ + source: 'www.google.com.*', + target: 'google.com', + remark: msg => msg.merge.target.remark.spec, +}, { + source: 'www.google.com.hk', + remark: msg => msg.merge.target.remark.blank +}, { + source: '**.*.google.com', + target: 2, + remark: msg => msg.merge.target.remark.integer +}] + +export const renderTargetTable = () => h(ElTable, { + data: TARGET_EXAMPLES, + border: true, + fit: true, +}, () => [ + h(ElTableColumn, { label: t(msg => msg.merge.sourceCol), width: 180 }, { + default: ({ row }: ElTableRowScope<_TargetExample>) => row.source + }), + h(ElTableColumn, { label: t(msg => msg.merge.targetCol), width: 150 }, { + default: ({ row }: ElTableRowScope<_TargetExample>) => computeTarget(row.target) + }), + h(ElTableColumn, { label: t(msg => msg.merge.target.lookCol), width: 240 }, { + default: ({ row }: ElTableRowScope<_TargetExample>) => renderTag(row) + }), + h(ElTableColumn, { label: t(msg => msg.merge.remarkCol), minWidth: 300 }, { + default: ({ row: { source, target, remark } }: ElTableRowScope<_TargetExample>) => tN(remark, { + source: h('i', source), + target: h('i', computeTarget(target)), + }) + }), +]) + +const MERGER = new CustomizedHostMergeRuler(TARGET_EXAMPLES.map( + ({ source: origin, target: merged }) => ({ origin, merged }) +)) + +const SITE_EXAMPLES: _SiteExample[] = [{ + original: 'www.google.com.au', + ruleIdx: 0, +}, { + original: 'www.google.com.pt', + ruleIdx: 0, +}, { + original: 'www.google.com.hk', + ruleIdx: [0, 1], + remark: t(msg => msg.merge.target.remark.specFirst), +}, { + original: 'es.news.google.com', + ruleIdx: [2], +}, { + original: 'a.b.c.phontos.google.com', + ruleIdx: [2], +}, { + original: 'pass.hust.edu.cn', + remark: tN(msg => msg.merge.target.remark.miss, { + link: link(PSL_HOMEPAGE), + }), +}] + +function renderHitCell({ ruleIdx }: _SiteExample): string | VNode { + const idxType = typeof ruleIdx + if (idxType === 'undefined') { + return '' + } else if (idxType === 'number') { + const rule = TARGET_EXAMPLES[ruleIdx as number] + return rule ? renderTag(rule) : '' + } else { + return h('span', + (ruleIdx as number[]) + .map(idx => TARGET_EXAMPLES[idx]) + .filter(a => !!a) + .map(renderTag) + ) + } +} + +export const renderSiteExampleTable = () => h(ElTable, { + data: SITE_EXAMPLES, + border: true, + fit: true, +}, () => [ + h(ElTableColumn, { + width: 195, + label: t(msg => msg.merge.target.originalCol), + formatter: (row: _SiteExample) => row.original, + }), + h(ElTableColumn, { + width: 160, + label: t(msg => msg.merge.target.mergedCol), + formatter: (row: _SiteExample) => MERGER.merge(row.original) + }), + h(ElTableColumn, { + width: 235, + label: t(msg => msg.merge.target.hitCol), + }, { + default: ({ row }: ElTableRowScope<_SiteExample>) => renderHitCell(row) + }), + h(ElTableColumn, { + label: t(msg => msg.merge.remarkCol) + }, { + default: ({ row }: ElTableRowScope<_SiteExample>) => row.remark + }) +]) \ No newline at end of file diff --git a/src/guide/component/privacy.ts b/src/guide/component/privacy.ts deleted file mode 100644 index c64545097..000000000 --- a/src/guide/component/privacy.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineComponent } from "vue" -import { h1, h2, list, paragraph, section } from "./common" - -const _default = defineComponent({ - setup() { - return () => section( - h1(msg => msg.layout.menu.privacy.title, 'privacy'), - h2(msg => msg.layout.menu.privacy.scope, 'privacy.scope'), - paragraph(msg => msg.privacy.scope.p1), - list( - msg => msg.privacy.scope.l1, - msg => msg.privacy.scope.l2, - msg => msg.privacy.scope.l3, - ), - h2(msg => msg.layout.menu.privacy.storage, 'privacy.storage'), - paragraph(msg => msg.privacy.storage.p1), - paragraph(msg => msg.privacy.storage.p2), - paragraph(msg => msg.privacy.storage.p3), - ) - } -}) - -export default _default \ No newline at end of file diff --git a/src/guide/component/privacy/index.ts b/src/guide/component/privacy/index.ts new file mode 100644 index 000000000..022de5095 --- /dev/null +++ b/src/guide/component/privacy/index.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { ElTableRowScope } from "@src/element-ui/table" + +import { defineComponent, h } from "vue" +import Article from "../common/article" +import { p, h2, alert } from "../common/util" +import { ElIcon, ElTable, ElTableColumn } from "element-plus" +import { PrivacyMessage, Scope } from "@i18n/message/guide/privacy" +import { t } from "@guide/locale" +import { CircleCheck, Warning } from "@element-plus/icons-vue" +import './privacy.sass' +import { START_ROUTE, APP_PAGE_ROUTE } from "@guide/router/constants" + +type ScopeRow = keyof PrivacyMessage['scope']['rows'] + +const ALL_ROWS: ScopeRow[] = ['website', 'tab', 'clipboard'] + +const renderTable = () => h(ElTable, { + data: ALL_ROWS, + border: true, + fit: true, + cellClassName: 'scope-table-cell', +}, () => [ + h(ElTableColumn, { + label: t(msg => msg.privacy.scope.cols.name), + minWidth: 200, + }, { + default: ({ row }: ElTableRowScope) => t(msg => msg.privacy.scope.rows[row].name) + }), + h(ElTableColumn, { + label: t(msg => msg.privacy.scope.cols.usage), + minWidth: 500, + }, { + default: ({ row }: ElTableRowScope) => t(msg => msg.privacy.scope.rows[row].usage) + }), + h(ElTableColumn, { + label: t(msg => msg.privacy.scope.cols.required), + width: 250 + }, { + default: ({ row }: ElTableRowScope) => { + const reason = t(msg => (msg.privacy.scope.rows[row] as Scope).optionalReason) + return h('span', { class: reason ? 'optional' : 'required' }, [ + h(ElIcon, () => h(reason ? Warning : CircleCheck)), + reason + ]) + } + }), +]) + +const _default = defineComponent(() => { + return () => h(Article, { + title: msg => msg.privacy.title, + previous: { route: START_ROUTE, title: msg => msg.start.title }, + next: { route: APP_PAGE_ROUTE, title: msg => msg.app.title }, + }, () => [ + alert(msg => msg.privacy.alert, 'warning'), + h2(msg => msg.privacy.scope.title), + renderTable(), + h2(msg => msg.privacy.storage.title), + p(msg => msg.privacy.storage.p1), + p(msg => msg.privacy.storage.p2), + alert(msg => msg.privacy.storage.p3, 'info'), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/privacy/privacy.sass b/src/guide/component/privacy/privacy.sass new file mode 100644 index 000000000..1599149b9 --- /dev/null +++ b/src/guide/component/privacy/privacy.sass @@ -0,0 +1,10 @@ +.el-table__row .cell .required svg + color: var(--el-color-success) +.el-table__row .cell .optional svg + color: var(--el-color-warning) + +.el-table__row .cell .el-icon + font-size: 16px + margin-right: 5px +.el-table .cell + word-break: normal !important diff --git a/src/guide/component/profile.ts b/src/guide/component/profile.ts deleted file mode 100644 index 9f1b6651e..000000000 --- a/src/guide/component/profile.ts +++ /dev/null @@ -1,32 +0,0 @@ - -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import { t } from "@guide/locale" -import { EDGE_HOMEPAGE, CHROME_HOMEPAGE, FIREFOX_HOMEPAGE, SOURCE_CODE_PAGE } from "@util/constant/url" -import { defineComponent } from "vue" - -import { h1, paragraph, link, section } from "./common" - -const _default = defineComponent({ - name: 'GuideProfile', - setup() { - return () => section( - h1(msg => msg.layout.menu.profile, 'profile', { appName: t(msg => msg.meta.name) }), - paragraph(msg => msg.profile.p1, { - edge: link(EDGE_HOMEPAGE, 'Edge'), - chrome: link(CHROME_HOMEPAGE, 'Chrome'), - firefox: link(FIREFOX_HOMEPAGE, 'Firefox'), - github: link(SOURCE_CODE_PAGE, 'Github'), - appName: t(msg => msg.meta.name), - }), - paragraph(msg => msg.profile.p2), - ) - } -}) - -export default _default \ No newline at end of file diff --git a/src/guide/component/start.ts b/src/guide/component/start.ts new file mode 100644 index 000000000..4503ac286 --- /dev/null +++ b/src/guide/component/start.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { img, p, h2, alert } from "./common/util" +import { getUrl } from "@api/chrome/runtime" +import { PRIVACY_ROUTE } from "@guide/router/constants" + +const _default = defineComponent(() => { + return () => h(Article, { + title: msg => msg.start.title, + next: { title: msg => msg.privacy.title, route: PRIVACY_ROUTE }, + }, () => [ + p(msg => msg.start.p1), + h2(msg => msg.start.s1), + p(msg => msg.start.s1p1), + img('pin.png', { height: 300 }), + h2(msg => msg.start.s2), + p(msg => msg.start.s2p1, { + demo: h('img', { src: getUrl('static/images/guide/beating.gif') }) + }), + h2(msg => msg.start.s3), + p(msg => msg.start.s3p1), + alert(msg => msg.start.alert, 'success'), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/usage.ts b/src/guide/component/usage.ts deleted file mode 100644 index b634e1032..000000000 --- a/src/guide/component/usage.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { getAppPageUrl, PSL_HOMEPAGE } from "@util/constant/url" -import { defineComponent } from "vue" -import { h1, h2, paragraph, list, link, section, linkInner } from "./common" -import { t } from "../locale" - -const quickstart = () => [ - h2(msg => msg.layout.menu.usage.quickstart, 'usage.quickstart'), - paragraph(msg => msg.usage.quickstart.p1), - list( - msg => msg.usage.quickstart.l1, - msg => msg.usage.quickstart.l2, - msg => msg.usage.quickstart.l3, - ), - paragraph(msg => msg.usage.quickstart.p2), -] - -const backgroundPageUrl = getAppPageUrl(false) - -const background = () => [ - h2(msg => msg.layout.menu.usage.background, 'usage.background'), - paragraph(msg => msg.usage.background.p1, { - background: linkInner(backgroundPageUrl, t(msg => msg.usage.background.backgroundPage)) - }), - list( - [msg => msg.usage.background.l1, { allFunction: t(msg => msg.base.allFunction) }], - [msg => msg.usage.background.l2, { allFunction: t(msg => msg.base.allFunction) }], - ), - paragraph(msg => msg.usage.background.p2), -] - -const advanced = () => [ - h2(msg => msg.layout.menu.usage.advanced, 'usage.advanced'), - paragraph(msg => msg.usage.advanced.p1), - list( - msg => msg.usage.advanced.l1, - msg => msg.usage.advanced.l2, - msg => msg.usage.advanced.l3, - msg => msg.usage.advanced.l4, - [msg => msg.usage.advanced.l5, { - psl: link(PSL_HOMEPAGE, 'Public Suffix List') - }], - msg => msg.usage.advanced.l6, - msg => msg.usage.advanced.l7, - msg => msg.usage.advanced.l8, - ), -] - -const backup = () => [ - h2(msg => msg.layout.menu.usage.backup, 'usage.backup'), - paragraph(msg => msg.usage.backup.p1, { gist: link('https://gist.github.com/', 'Github Gist') }), - list( - [msg => msg.usage.backup.l1, { - token: link('https://github.com/settings/tokens', 'token') - }], - msg => msg.usage.backup.l2, - msg => msg.usage.backup.l3, - ), -] - -const _default = defineComponent({ - setup() { - return () => section( - h1(msg => msg.layout.menu.usage.title, 'usage'), - ...quickstart(), - ...background(), - ...advanced(), - ...backup(), - ) - } -}) - -export default _default \ No newline at end of file diff --git a/src/guide/component/virtual.ts b/src/guide/component/virtual.ts new file mode 100644 index 000000000..014afc5e4 --- /dev/null +++ b/src/guide/component/virtual.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { LIMIT_ROUTE, MERGE_ROUTE } from "@guide/router/constants" +import { p, h2, ul, appLink } from "./common/util" +import { ElTag } from "element-plus" +import { t } from "@guide/locale" + +const demoTag = (demoSite: string) => h(ElTag, { size: 'small', type: 'info' }, () => demoSite) + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: MERGE_ROUTE, + title: msg => msg.merge.title + }, + next: { + route: LIMIT_ROUTE, + title: msg => msg.limit.title + }, + title: msg => msg.virtual.title, + }, () => [ + p(msg => msg.virtual.p1), + h2(msg => msg.virtual.step.title), + ul( + [msg => msg.virtual.step.enter, { link: appLink(), menuItem: t(msg => msg.appMenu.siteManage) }], + msg => msg.virtual.step.click, + [msg => msg.virtual.step.form, { + demo1: demoTag('github.com/sheepzh'), + demo2: demoTag('github.com/sheepzh/timer/**'), + }], + msg => msg.virtual.step.browse, + ), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/guide.d.ts b/src/guide/guide.d.ts deleted file mode 100644 index a14023b1c..000000000 --- a/src/guide/guide.d.ts +++ /dev/null @@ -1,14 +0,0 @@ - -/** - * The anchor of menu items - */ -declare type Position = - | 'profile' - | 'usage' - | 'usage.quickstart' - | 'usage.background' - | 'usage.advanced' - | 'usage.backup' - | 'privacy' - | 'privacy.scope' - | 'privacy.storage' \ No newline at end of file diff --git a/src/guide/index.ts b/src/guide/index.ts index 3334af4c8..0c6fc7c9c 100644 --- a/src/guide/index.ts +++ b/src/guide/index.ts @@ -1,30 +1,36 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import type { Language } from "element-plus/lib/locale" import "./style" -import 'element-plus/theme-chalk/index.css' - -import { initLocale } from "@i18n" +import ElementPlus from 'element-plus' +import { initLocale, locale } from "@i18n" import { t } from "./locale" -import { init as initTheme, toggle } from "@util/dark-mode" +import installRouter from "./router" import { createApp } from "vue" import Main from "./layout" -import optionService from "@service/option-service" +import 'element-plus/theme-chalk/index.css' + +const locales: { [locale in timer.Locale]: () => Promise<{ default: Language }> } = { + zh_CN: () => import('element-plus/lib/locale/lang/zh-cn'), + zh_TW: () => import('element-plus/lib/locale/lang/zh-tw'), + en: () => import('element-plus/lib/locale/lang/en'), + ja: () => import('element-plus/lib/locale/lang/ja'), + pt_PT: () => import('element-plus/lib/locale/lang/pt'), +} async function main() { - initTheme() - // Calculate the latest mode - optionService.isDarkMode().then(toggle) await initLocale() - const app = createApp(Main) - app.mount('#guide') + installRouter(app) document.title = t(msg => msg.base.guidePage) + ' | ' + t(msg => msg.meta.name) + locales[locale]?.()?.then(msg => app.use(ElementPlus, { locale: msg.default })) + app.mount('#guide') } main() diff --git a/src/guide/layout/content.ts b/src/guide/layout/content.ts deleted file mode 100644 index 91217a2f1..000000000 --- a/src/guide/layout/content.ts +++ /dev/null @@ -1,34 +0,0 @@ - -import { ElDivider } from "element-plus" -import { watch, defineComponent, h, onMounted, PropType } from "vue" -import Profile from "../component/profile" -import Usage from "../component/usage" -import Privacy from "../component/privacy" -import { position2AnchorClz } from "@guide/util" - -function scrollPosition(position: Position) { - document.querySelector(`.${position2AnchorClz(position)}`)?.scrollIntoView?.() -} - -const _default = defineComponent({ - name: 'GuideContent', - props: { - position: { - type: String as PropType, - required: false, - } - }, - setup(props) { - onMounted(() => scrollPosition(props.position)) - watch(() => props.position, newVal => newVal && scrollPosition(newVal)) - return () => [ - h(Profile), - h(ElDivider), - h(Usage), - h(ElDivider), - h(Privacy), - ] - } -}) - -export default _default \ No newline at end of file diff --git a/src/guide/layout/header/dark-switch.ts b/src/guide/layout/header/dark-switch.ts new file mode 100644 index 000000000..47ea08aed --- /dev/null +++ b/src/guide/layout/header/dark-switch.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { Ref } from "vue" + +import { defineComponent, ref, h } from "vue" +import { init as initTheme, toggle } from "@util/dark-mode" +import optionService from "@service/option-service" +import { ElSwitch } from "element-plus" +import { Moon, Sunrise } from "@element-plus/icons-vue" + +const _default = defineComponent(() => { + const value: Ref = ref(initTheme()) + const handleChange = (newVal: boolean) => { + toggle(newVal) + value.value = newVal + } + // Calculate the latest mode + optionService.isDarkMode().then(handleChange) + return () => h(ElSwitch, { + modelValue: value.value, + inactiveIcon: Sunrise, + activeIcon: Moon, + inlinePrompt: true, + async onChange(newVal: boolean) { + handleChange(newVal) + const option = await optionService.getAllOption() + option.darkMode = newVal ? 'on' : 'off' + optionService.setAppearanceOption(option) + }, + }) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/header.sass b/src/guide/layout/header/header.sass new file mode 100644 index 000000000..750e4c101 --- /dev/null +++ b/src/guide/layout/header/header.sass @@ -0,0 +1,71 @@ + +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +$logoSize: 42px +$iconSize: 24px +$localeIconSize: 19px + +.header-container + line-height: 60px + height: 60px + display: flex + justify-content: space-between + margin: 0 auto + .header-left + display: flex + box-sizing: content-box + cursor: pointer + .icon,.title + height: $logoSize + line-height: $logoSize + display: inline-block + margin: auto 0 + .icon + margin-right: 10px + width: $logoSize + .title + font-size: 20px + color: var(--el-text-color-primary) + .header-right + .el-switch__inner + svg path + fill: var(--el-color-white) + svg + width: $iconSize + height: $iconSize + path + fill: var(--el-text-color-regular) + .el-switch + --el-switch-off-color: var(--el-color-primary) + // = [light-theme] --el-text-color-regular + --el-switch-on-color: #606266 + margin-left: 10px + .el-switch__core + width: 45px + .icon-link + display: inline-flex + background-color: transparent + height: $iconSize + border: none + padding: 0px + .locale-select, .icon-link + vertical-align: middle + margin-left: 10px + .locale-select + .locale-button + display: flex + svg + width: $localeIconSize + height: $localeIconSize + span + font-size: calc( $localeIconSize - 3px ) + padding-left: 3px + line-height: $localeIconSize + color: var(--el-text-color-regular) + .locale-button:focus-visible + outline: none diff --git a/src/guide/layout/header/icon-button.ts b/src/guide/layout/header/icon-button.ts new file mode 100644 index 000000000..d6c3c8654 --- /dev/null +++ b/src/guide/layout/header/icon-button.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { ElTooltip } from "element-plus" +import { defineComponent, h } from "vue" +import SvgIcon from "./svg-icon" + +const _default = defineComponent({ + props: { + path: String, + tip: String, + href: String, + }, + setup(props) { + return () => h(ElTooltip, { + effect: 'dark', + placement: 'bottom', + content: props.tip, + offset: 5, + showArrow: false, + }, () => h('a', { + href: props.href, + target: '_blank', + class: 'icon-link', + }, h(SvgIcon, { path: props.path }))) + } +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/index.ts b/src/guide/layout/header/index.ts new file mode 100644 index 000000000..abcb1cc48 --- /dev/null +++ b/src/guide/layout/header/index.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@guide/locale" +import { defineComponent, h } from "vue" +import IconButton from "./icon-button" +import DarkSwitch from "./dark-switch" +import LocaleSelect from "./locale-select" +import './header.sass' +import { GITHUB_PATH, EMAIL_PATH } from "./svg" +import { useRouter } from "vue-router" +import { HOME_ROUTE } from "@guide/router/constants" +import { SOURCE_CODE_PAGE } from "@util/constant/url" +import { AUTHOR_EMAIL } from "@src/package" + +const ICON_URL = chrome.runtime.getURL("static/images/icon.png") + +const logo = () => h('span', { class: 'icon' }, h('img', { src: ICON_URL, width: 42, height: 42 })) +const title = () => h('span', { class: 'title' }, t(msg => msg.meta.marketName)) +const github = () => h(IconButton, { + path: GITHUB_PATH, + tip: t(msg => msg.layout.header.sourceCode), + href: SOURCE_CODE_PAGE, +}) +const email = () => h(IconButton, { + path: EMAIL_PATH, + tip: t(msg => msg.layout.header.email), + href: `mailto:${AUTHOR_EMAIL}` +}) +const darkSwitch = () => h(DarkSwitch) +const localeSelect = () => h(LocaleSelect) + +const _default = defineComponent(() => { + const router = useRouter() + return () => h('div', { class: 'header-container' }, [ + h('div', { class: 'header-left', onClick: () => router.push(HOME_ROUTE) }, [logo(), title()]), + h('div', { class: 'header-right' }, [ + github(), + email(), + darkSwitch(), + localeSelect(), + ]) + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/locale-select.ts b/src/guide/layout/header/locale-select.ts new file mode 100644 index 000000000..cfb381a2f --- /dev/null +++ b/src/guide/layout/header/locale-select.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { ElDropdown, ElDropdownItem, ElDropdownMenu } from "element-plus" +import { defineComponent, h } from "vue" +import { getI18nVal, locale as current, ALL_LOCALES } from "@i18n" +import localeMessages from "@i18n/message/common/locale" +import SvgIcon from "./svg-icon" +import { LOCALE_PATH } from "./svg" +import optionService from "@service/option-service" +import { createTabAfterCurrent } from "@api/chrome/tab" +import { CROWDIN_HOMEPAGE } from "@util/constant/url" + +const HELP_CMD: string = '_help' + +const getLocaleName = (locale: timer.Locale) => getI18nVal(localeMessages, msg => msg.name, locale) + +const renderIcon = () => h('div', + { class: 'locale-button' }, + [ + h(SvgIcon, { path: LOCALE_PATH }), + h('span', getLocaleName(current)) + ] +) + +const renderLocaleItem = (locale: timer.Locale) => h(ElDropdownItem, { + disabled: current === locale, + command: locale, +}, () => getLocaleName(locale)) + +const renderHelpItem = () => h(ElDropdownItem, { + divided: true, + command: HELP_CMD, +}, () => 'Help translate!') + +const renderMenuItems = () => h(ElDropdownMenu, + () => [ + ...ALL_LOCALES.map(renderLocaleItem), + renderHelpItem(), + ] +) + +const handleCommand = async (cmd: string) => { + if (cmd === HELP_CMD) { + createTabAfterCurrent(CROWDIN_HOMEPAGE) + return + } + const locale = cmd as timer.Locale + const option = await optionService.getAllOption() + option.locale = locale + await optionService.setAppearanceOption(option) + window.location.reload?.() +} + +const _default = defineComponent(() => { + return () => h(ElDropdown, { + class: 'locale-select', + onCommand: handleCommand + }, { + default: () => renderIcon(), + dropdown: () => renderMenuItems() + }) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/svg-icon.ts b/src/guide/layout/header/svg-icon.ts new file mode 100644 index 000000000..2511e6e52 --- /dev/null +++ b/src/guide/layout/header/svg-icon.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" + +const _default = defineComponent({ + props: { + path: String, + }, + setup(props) { + return () => h('svg', { + viewBox: "0 0 1024 1024", + xmlns: "http://www.w3.org/2000/svg" + }, h("path", { d: props.path })) + } +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/svg.ts b/src/guide/layout/header/svg.ts new file mode 100644 index 000000000..4fa03b3ff --- /dev/null +++ b/src/guide/layout/header/svg.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export const GITHUB_PATH = 'M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9 23.5 23.2 38.1 55.4 38.1 91v112.5c0.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z' +export const EMAIL_PATH = 'M853.333333 170.666667H170.666667c-47.146667 0-84.906667 38.186667-84.906667 85.333333L85.333333 768c0 47.146667 38.186667 85.333333 85.333334 85.333333h682.666666c47.146667 0 85.333333-38.186667 85.333334-85.333333V256c0-47.146667-38.186667-85.333333-85.333334-85.333333z m0 170.666666L512 554.666667 170.666667 341.333333v-85.333333l341.333333 213.333333 341.333333-213.333333v85.333333z' +export const LOCALE_PATH = 'M540.245333 645.888L426.410667 522.069333l1.365333-1.536a889.770667 889.770667 0 0 0 166.314667-322.048h131.413333V99.669333H411.562667V1.109333h-89.6v98.645334H7.850667v98.730666h500.906666a797.696 797.696 0 0 1-142.165333 263.936 778.581333 778.581333 0 0 1-103.594667-165.290666h-89.6c33.621333 82.176 78.677333 158.037333 133.546667 224.938666L78.848 769.706667l63.658667 69.973333 224.170666-246.613333 139.52 153.429333 34.133334-100.608z m252.501334-250.026667H703.061333l-201.813333 591.872h89.685333l50.261334-147.968h212.992l50.688 147.968h89.685333L792.746667 395.776zM675.242667 741.034667l72.618666-213.674667 72.704 213.674667H675.242667z' diff --git a/src/guide/layout/index.ts b/src/guide/layout/index.ts index 7cec2f643..fa3ab421b 100644 --- a/src/guide/layout/index.ts +++ b/src/guide/layout/index.ts @@ -1,28 +1,28 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import type { Ref } from "vue" - -import { ElContainer, ElAside, ElMain } from "element-plus" -import { defineComponent, ref, h } from "vue" -import Content from "./content" +import { ElContainer, ElAside, ElMain, ElHeader, ElScrollbar } from "element-plus" +import { defineComponent, h } from "vue" +import Header from "./header" import Menu from "./menu" +import { RouterView, useRoute } from "vue-router" +import { HOME_ROUTE } from "@guide/router/constants" + +const renderMain = () => h(ElContainer, {}, () => [ + h(ElAside, () => h(ElScrollbar, () => h(Menu))), + h(ElMain, () => h(ElScrollbar, () => h(RouterView))), +]) -const _default = defineComponent({ - name: "Guide", - render() { - const position: Ref = ref() - return h(ElContainer, { class: 'guide-container' }, () => [ - h(ElAside, {}, () => h(Menu, { onClick: (newPosition: Position) => position.value = newPosition })), - h(ElContainer, { - id: 'app-body' - }, () => h(ElMain, {}, () => h(Content, { position: position.value }))) - ]) - } +const _default = defineComponent(() => { + const route = useRoute() + return () => h(ElContainer, { class: 'guide-container' }, () => [ + h(ElHeader, () => h(Header)), + route.path === HOME_ROUTE ? h(RouterView) : renderMain(), + ]) }) export default _default \ No newline at end of file diff --git a/src/guide/layout/menu.ts b/src/guide/layout/menu.ts index 21db6fd1c..1688faae2 100644 --- a/src/guide/layout/menu.ts +++ b/src/guide/layout/menu.ts @@ -5,118 +5,87 @@ * https://opensource.org/licenses/MIT */ -import { ref, VNode } from "vue" -import ElementIcon from "@src/element-ui/icon" -import { I18nKey, t } from "@guide/locale" -import { ElIcon, ElMenu, ElMenuItem, ElSubMenu } from "element-plus" -import { defineComponent, h } from "vue" -import { User, Memo, MagicStick } from "@element-plus/icons-vue" +import type { Ref } from "vue" +import type { I18nKey } from "@guide/locale" -type _Item = { - title: I18nKey - position: Position -} +import { t } from "@guide/locale" +import { ElMenu, ElMenuItem, ElSubMenu } from "element-plus" +import { defineComponent, h, onMounted, ref } from "vue" +import { Router, useRouter } from "vue-router" +import { + START_ROUTE, + PRIVACY_ROUTE, + USAGE_ROUTE, + APP_PAGE_ROUTE, + MERGE_ROUTE, + VIRTUAL_ROUTE, + BACKUP_ROUTE, + LIMIT_ROUTE, +} from "@guide/router/constants" -type _Group = { +type MenuConf = { + route: string title: I18nKey - position: Position - children: _Item[] - icon: ElementIcon + children?: MenuConf[] } -const quickstartPosition: Position = 'usage.quickstart' -const profilePosition: Position = 'profile' -const menus: _Group[] = [ - { - title: msg => msg.layout.menu.usage.title, - position: 'usage', - children: [ - { - title: msg => msg.layout.menu.usage.quickstart, - position: quickstartPosition - }, { - title: msg => msg.layout.menu.usage.background, - position: 'usage.background' - }, { - title: msg => msg.layout.menu.usage.advanced, - position: 'usage.advanced' - }, { - title: msg => msg.layout.menu.usage.backup, - position: 'usage.backup', - } - ], - icon: Memo - }, - { - title: msg => msg.layout.menu.privacy.title, - position: 'privacy', - icon: User, - children: [ - { - title: msg => msg.layout.menu.privacy.scope, - position: 'privacy.scope' - }, - { - title: msg => msg.layout.menu.privacy.storage, - position: 'privacy.storage' - }, - ], +const MENU_CONFS: MenuConf[] = [{ + route: START_ROUTE, + title: msg => msg.start.title, +}, { + route: PRIVACY_ROUTE, + title: msg => msg.privacy.title, +}, { + route: USAGE_ROUTE, + title: msg => msg.layout.menu.usage, + children: [{ + route: APP_PAGE_ROUTE, + title: msg => msg.app.title, + }, { + route: MERGE_ROUTE, + title: msg => msg.merge.title, + }, { + route: VIRTUAL_ROUTE, + title: msg => msg.virtual.title, + }, { + route: LIMIT_ROUTE, + title: msg => msg.limit.title, + }, { + route: BACKUP_ROUTE, + title: msg => msg.backup.title + }], +}] +function renderWithConf(conf: MenuConf, router: Router, activeRoute: Ref) { + const { route, title, children } = conf + if (children?.length) { + return h(ElSubMenu, { + index: route, + }, { + title: () => h('span', t(title)), + default: () => children.map(child => renderWithConf(child, router, activeRoute)) + }) } -] - -function renderMenuItem(handleClick: (position: Position) => void, item: _Item, index: number): VNode { - const { title, position } = item return h(ElMenuItem, { - index: position, - onClick: () => handleClick(position) - }, () => h('span', {}, `${index + 1}. ${t(title)}`)) + index: route, + onClick: () => { + router.push(route) + activeRoute.value = route + }, + }, () => h('span', t(title))) } -function renderGroup(handleClick: (position: Position) => void, group: _Group): VNode { - const { position, title, children, icon } = group - return h(ElSubMenu, { - index: position, - }, { - title: () => [ - h(ElIcon, () => h(icon)), - h('span', {}, t(title)) - ], - default: () => children.map( - (item, index) => renderMenuItem(handleClick, item, index) - ) - }) -} - -const _default = defineComponent({ - name: "GuideMenu", - emits: { - click: (_position: Position) => true, - }, - setup(_, ctx) { - const handleClick = (position: Position) => ctx.emit('click', position) - const menuItems = () => [ - h(ElMenuItem, { - index: profilePosition, - onClick: () => handleClick(profilePosition) - }, () => [ - h(ElIcon, () => h(MagicStick)), - h('span', {}, t(msg => msg.layout.menu.profile, { appName: t(msg => msg.meta.name) })) - ]), - ...menus.map( - group => renderGroup(handleClick, group) - ) - ] - const menuRef = ref() - return () => h(ElMenu, { - defaultActive: profilePosition, - defaultOpeneds: menus.map(group => group.position), - ref: menuRef, - onClose(index: string) { - menuRef.value?.open?.(index) - } - }, menuItems) - } +const _default = defineComponent(() => { + const router = useRouter() + const activeRoute: Ref = ref() + // Initialize current route in a new macro task + onMounted(() => setTimeout(() => activeRoute.value = router.currentRoute?.value?.path)) + return () => [ + h(ElMenu, { + defaultOpeneds: MENU_CONFS.filter(m => m.children?.length).map(group => group.route), + defaultActive: activeRoute.value, + }, () => MENU_CONFS.map(conf => renderWithConf(conf, router, activeRoute))) + ] }) export default _default \ No newline at end of file diff --git a/src/guide/router/constants.ts b/src/guide/router/constants.ts new file mode 100644 index 000000000..484151ba8 --- /dev/null +++ b/src/guide/router/constants.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export const HOME_ROUTE = '/home' +export const START_ROUTE = '/start' +export const PRIVACY_ROUTE = '/privacy' +export const USAGE_ROUTE = '/usage' +export const APP_PAGE_ROUTE = '/usage/management' +export const MERGE_ROUTE = '/usage/merge' +export const VIRTUAL_ROUTE = '/usage/virtual' +export const LIMIT_ROUTE = '/usage/limit' +export const BACKUP_ROUTE = '/usage/backup' \ No newline at end of file diff --git a/src/guide/router/index.ts b/src/guide/router/index.ts new file mode 100644 index 000000000..0c248b1cd --- /dev/null +++ b/src/guide/router/index.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { RouteRecordRaw } from "vue-router" + +import { App } from "vue" +import { createRouter, createWebHashHistory } from "vue-router" +import { + HOME_ROUTE, + START_ROUTE, + PRIVACY_ROUTE, + USAGE_ROUTE, + APP_PAGE_ROUTE, + MERGE_ROUTE, + VIRTUAL_ROUTE, + LIMIT_ROUTE, + BACKUP_ROUTE, +} from "./constants" + + +const routes: RouteRecordRaw[] = [{ + path: '/', + redirect: HOME_ROUTE, +}, { + path: HOME_ROUTE, + component: () => import('../component/home'), +}, { + path: START_ROUTE, + component: () => import('../component/start'), +}, { + path: PRIVACY_ROUTE, + component: () => import('../component/privacy') +}, { + path: USAGE_ROUTE, + redirect: APP_PAGE_ROUTE, +}, { + path: APP_PAGE_ROUTE, + component: () => import('../component/app'), +}, { + path: MERGE_ROUTE, + component: () => import('../component/merge'), +}, { + path: VIRTUAL_ROUTE, + component: () => import('../component/virtual'), +}, { + path: LIMIT_ROUTE, + component: () => import('../component/limit'), +}, { + path: BACKUP_ROUTE, + component: () => import('../component/backup'), +}] + +const router = createRouter({ + history: createWebHashHistory(), + routes, +}) + +export default (app: App) => { + app.use(router) +} diff --git a/src/guide/style/dark-theme.sass b/src/guide/style/dark-theme.sass index 38b9ef1cf..d426eb9e7 100644 --- a/src/guide/style/dark-theme.sass +++ b/src/guide/style/dark-theme.sass @@ -77,4 +77,5 @@ html[data-theme='dark']:root --el-mask-color-extra-light: rgba(0 0 0 .3) --guide-container-bg-color: var(--el-fill-color-dark) // menu - --el-menu-bg-color: var(--el-fill-color-light) + --el-menu-bg-color: var(--guide-container-bg-color) + --guide-article-title-color: #DDDDDD diff --git a/src/guide/style/index.sass b/src/guide/style/index.sass index b1a9ffe1f..5acf906d8 100644 --- a/src/guide/style/index.sass +++ b/src/guide/style/index.sass @@ -7,6 +7,11 @@ @import './light-theme' @import './dark-theme' +@import '../component/common/article' + +$headerHeight: 61px +$contentHeight: calc(100vh - $headerHeight) +$asideWidth: 360px body margin: 0px @@ -15,31 +20,38 @@ body display: flex height: 100vh width: 100% - scroll-behavior: smooth + .el-header + position: relative + width: 100% + height: 60px + border-bottom: 1px var(--el-border-color) var(--el-border-style) + padding: 0 30px + .el-aside + display: block + position: absolute + left: 0 + top: $headerHeight + bottom: 0 + height: $contentHeight + width: $asideWidth + padding-top: 15px + padding-left: 15px + .el-menu + border-right: none .guide-container background: var(--guide-container-bg-color) - .el-menu - height: 100% + overflow-y: auto .el-main - height: 100% - padding: 10px 0 + position: absolute + left: $asideWidth + right: 0px + top: $headerHeight + bottom: 0 + padding: 30px 20px + // 以下待删除 .guide-area padding: 0 9vw padding-bottom: 10px - h1,h2,span,div,li - color: var(--el-text-color-primary) - h1 - font-weight: 500 - font-size: 1.8rem - letter-spacing: 0 - line-height: 1.333 - h2 - font-weight: 500 - font-size: 1.4rem - letter-spacing: 0 - line-height: 1.333 - margin: 40px 0 - margin-left: 2px .guide-paragragh,.guide-list font-size: 0.9375rem font-weight: 400 diff --git a/src/guide/style/light-theme.sass b/src/guide/style/light-theme.sass index e75ed01cf..a48602d7e 100644 --- a/src/guide/style/light-theme.sass +++ b/src/guide/style/light-theme.sass @@ -6,7 +6,5 @@ */ \:root - --el-menu-bg-color: #1d222d - --el-menu-text-color: #c1c6c8 - --el-menu-hover-bg-color: #262f3e --guide-container-bg-color: var(--el-fill-color-blank) + --guide-article-title-color: #222222 diff --git a/src/guide/util.ts b/src/guide/util.ts deleted file mode 100644 index 474f93566..000000000 --- a/src/guide/util.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function position2AnchorClz(position: Position): string { - return `anchor-${position?.replace('.', '-')}` -} \ No newline at end of file diff --git a/src/i18n/chrome/index.ts b/src/i18n/chrome/index.ts index f655f9520..a4e9fc222 100644 --- a/src/i18n/chrome/index.ts +++ b/src/i18n/chrome/index.ts @@ -17,7 +17,8 @@ const _default: { [locale in FakedLocale]: any } = { zh_CN: compile(messages.zh_CN), zh_TW: compile(messages.zh_TW), en: compile(messages.en), - ja: compile(messages.ja) + ja: compile(messages.ja), + pt_PT: compile(messages.pt_PT) } export default _default \ No newline at end of file diff --git a/src/i18n/chrome/message.ts b/src/i18n/chrome/message.ts index 9ef326c60..7fde80765 100644 --- a/src/i18n/chrome/message.ts +++ b/src/i18n/chrome/message.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -8,7 +8,7 @@ import metaMessages, { MetaMessage } from "../message/common/meta" import contextMenusMessages, { ContextMenusMessage } from "../message/common/context-menus" import initialMessages, { InitialMessage } from "../message/common/initial" -import baseMessages, { BaseMessage } from "@i18n/message/common/base" +import baseMessages, { BaseMessage } from "../message/common/base" export type ChromeMessage = { meta: MetaMessage @@ -41,6 +41,12 @@ const messages: Messages = { base: baseMessages.ja, contextMenus: contextMenusMessages.ja, initial: initialMessages.ja, + }, + pt_PT: { + meta: metaMessages.pt_PT, + base: baseMessages.pt_PT, + contextMenus: contextMenusMessages.pt_PT, + initial: initialMessages.pt_PT, } } @@ -51,6 +57,7 @@ const placeholder: ChromeMessage = { name: '', description: '', marketName: '', + slogan: '', }, base: { currentVersion: '', diff --git a/src/i18n/index.ts b/src/i18n/index.ts index d23bf7190..846492bb2 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -16,15 +16,21 @@ export type FakedLocale = timer.Locale */ const FEEDBACK_LOCALE: timer.Locale = "en" +export const ALL_LOCALES: timer.Locale[] = ['en', 'zh_CN', 'zh_TW', 'ja', 'pt_PT'] + export const defaultLocale: timer.Locale = "zh_CN" // Standardize the locale code according to the Chrome locale code const chrome2I18n: { [key: string]: timer.Locale } = { 'zh-CN': "zh_CN", 'zh-TW': "zh_TW", + 'en': 'en', 'en-US': "en", 'en-GB': "en", 'ja': "ja", + 'pt': 'pt_PT', + 'pt-PT': 'pt_PT', + 'pt-BR': 'pt_PT', } const translationChrome2I18n: { [key: string]: timer.TranslatingLocale } = { @@ -33,8 +39,6 @@ const translationChrome2I18n: { [key: string]: timer.TranslatingLocale } = { "es-MX": 'es', ko: 'ko', pl: 'pl', - "pt-PT": 'pt', - "pt-BR": 'pt_BR', ru: 'ru', uk: 'uk', fr: 'fr', diff --git a/src/i18n/message/app/analysis-resource.json b/src/i18n/message/app/analysis-resource.json new file mode 100644 index 000000000..47680d249 --- /dev/null +++ b/src/i18n/message/app/analysis-resource.json @@ -0,0 +1,137 @@ +{ + "zh_CN": { + "common": { + "focusTotal": "总计浏览时长", + "visitTotal": "总计访问次数", + "ringGrowth": "与上期相比 {value}", + "merged": "合并", + "virtual": "自定义", + "hostPlaceholder": "搜索你想分析的站点", + "emptyDesc": "未选择站点" + }, + "summary": { + "title": "数据总览", + "day": "总计活跃天数", + "firstDay": "首次访问 {value}" + }, + "trend": { + "title": "区间趋势", + "activeDay": "活跃天数", + "totalDay": "区间总天数", + "maxFocus": "单日最大浏览时长", + "averageFocus": "单日平均浏览时长", + "maxVisit": "单日最大访问次数", + "averageVisit": "单日平均访问次数", + "focusTitle": "浏览时长趋势", + "visitTitle": "访问次数趋势" + } + }, + "zh_TW": { + "common": { + "focusTotal": "總計瀏覽時長", + "visitTotal": "總計訪問次數", + "ringGrowth": "與前期相比 {value}", + "merged": "合並", + "virtual": "自定義", + "hostPlaceholder": "蒐索你想分析的站點", + "emptyDesc": "未選擇站點" + }, + "summary": { + "title": "數據總覽", + "day": "總計活躍天數", + "firstDay": "首次訪問 {value}" + }, + "trend": { + "title": "區間趨勢", + "activeDay": "活躍天數", + "totalDay": "區間總天數", + "maxFocus": "單日最大瀏覽時長", + "averageFocus": "單日平均瀏覽時長", + "maxVisit": "單日最大訪問次數", + "averageVisit": "單日平均訪問次數", + "focusTitle": "瀏覽時長趨勢", + "visitTitle": "訪問次數趨勢" + } + }, + "en": { + "common": { + "focusTotal": "Total browsing time", + "visitTotal": "Total visits", + "ringGrowth": "{value} compared to the previous period", + "merged": "Merged", + "virtual": "Virtual", + "hostPlaceholder": "Search for a site to analyze", + "emptyDesc": "No site selected" + }, + "summary": { + "title": "Summary", + "day": "Total active days", + "firstDay": "First visit {value}" + }, + "trend": { + "title": "Trends", + "activeDay": "Active days", + "totalDay": "Period days", + "maxFocus": "Daily maximum browsing time", + "averageFocus": "Daily average browsing time", + "maxVisit": "Daily maximum visits", + "averageVisit": "Daily average visits", + "focusTitle": "Browsing Time Trends", + "visitTitle": "Visit Trends" + } + }, + "ja": { + "common": { + "focusTotal": "総閲覧時間", + "visitTotal": "総訪問数", + "ringGrowth": "前期比 {value}", + "merged": "合并", + "virtual": "カスタマイズ", + "hostPlaceholder": "ドメイン名を検索", + "emptyDesc": "サイトは空です" + }, + "summary": { + "title": "まとめ", + "day": "合計アクティブ日数", + "firstDay": "{value} を初めて訪問する" + }, + "trend": { + "title": "レンジトレンド", + "activeDay": "アクティブな日", + "totalDay": "間隔の合計日数", + "maxFocus": "1 日の最大閲覧時間", + "averageFocus": "1 日あたりの平均閲覧時間", + "maxVisit": "1 日あたりの最大訪問数", + "averageVisit": "1 日あたりの平均訪問数", + "focusTitle": "タイム トレンドの閲覧", + "visitTitle": "訪問数の傾向" + } + }, + "pt_PT": { + "common": { + "focusTotal": "Tempo total de navegação", + "visitTotal": "Total de visitas", + "ringGrowth": "{value} em comparação com o período anterior", + "merged": "Mesclado", + "virtual": "Virtual", + "hostPlaceholder": "Procure um site para analisar", + "emptyDesc": "Nenhum site selecionado" + }, + "summary": { + "title": "Resumo", + "day": "Total de dias ativos", + "firstDay": "Primeira visita {value}" + }, + "trend": { + "title": "Tendências", + "activeDay": "Dias ativos", + "totalDay": "Dias de período", + "maxFocus": "Tempo máximo de navegação diário", + "averageFocus": "Tempo médio de navegação diário", + "maxVisit": "Máximo diário de visitas", + "averageVisit": "Média diária de visitas", + "focusTitle": "Tendências do Tempo de Navegação", + "visitTitle": "Tendências de Visita" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/app/analysis.ts b/src/i18n/message/app/analysis.ts index 21b186a67..d9cdd068f 100644 --- a/src/i18n/message/app/analysis.ts +++ b/src/i18n/message/app/analysis.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './analysis-resource.json' + export type AnalysisMessage = { common: { focusTotal: string @@ -22,12 +24,6 @@ export type AnalysisMessage = { } trend: { title: string - startDate: string, - endDate: string - lastWeek: string - last15Days: string - last30Days: string - last90Days: string activeDay: string totalDay: string maxFocus: string @@ -39,139 +35,6 @@ export type AnalysisMessage = { } } -const _default: Messages = { - zh_CN: { - common: { - focusTotal: '总计浏览时长', - visitTotal: '总计访问次数', - ringGrowth: '与上期相比 {value}', - merged: '合并', - virtual: '自定义', - hostPlaceholder: '搜索你想分析的站点', - emptyDesc: '未选择站点' - }, - summary: { - title: '数据总览', - day: '总计活跃天数', - firstDay: '首次访问 {value}', - }, - trend: { - title: '区间趋势', - startDate: '开始日期', - endDate: '结束日期', - lastWeek: '最近 7 天', - last15Days: '最近 15 天', - last30Days: '最近 30 天', - last90Days: '最近 90 天', - activeDay: '活跃天数', - totalDay: '区间总天数', - maxFocus: '单日最大浏览时长', - averageFocus: '单日平均浏览时长', - maxVisit: '单日最大访问次数', - averageVisit: '单日平均访问次数', - focusTitle: '浏览时长趋势', - visitTitle: '访问次数趋势', - } - }, - zh_TW: { - common: { - focusTotal: '總計瀏覽時長', - visitTotal: '總計訪問次數', - ringGrowth: '與前期相比 {value}', - merged: '合並', - virtual: '自定義', - hostPlaceholder: '蒐索你想分析的站點', - emptyDesc: '未選擇站點', - }, - summary: { - title: '數據總覽', - day: '總計活躍天數', - firstDay: '首次訪問 {value}', - }, - trend: { - title: '區間趨勢', - startDate: '開始日期', - endDate: '結束日期', - lastWeek: '最近 7 天', - last15Days: '最近 15 天', - last30Days: '最近 30 天', - last90Days: '最近 90 天', - activeDay: '活躍天數', - totalDay: '區間總天數', - maxFocus: '單日最大瀏覽時長', - averageFocus: '單日平均瀏覽時長', - maxVisit: '單日最大訪問次數', - averageVisit: '單日平均訪問次數', - focusTitle: '瀏覽時長趨勢', - visitTitle: '訪問次數趨勢', - } - }, - en: { - common: { - focusTotal: 'Total browsing time', - visitTotal: 'Total visits', - ringGrowth: '{value} compared to the previous period', - merged: 'Merged', - virtual: 'Virtual', - hostPlaceholder: 'Search for a site to analyze', - emptyDesc: 'No site selected', - }, - summary: { - title: 'Summary', - day: 'Total active days', - firstDay: 'First visit {value}', - }, - trend: { - title: 'Trends', - startDate: 'Start date', - endDate: 'End date', - lastWeek: 'Last week', - last15Days: 'Last 15 days', - last30Days: 'Last 30 days', - last90Days: 'Last 90 days', - activeDay: 'Active days', - totalDay: 'Period days', - maxFocus: 'Daily maximum browsing time', - averageFocus: 'Daily average browsing time', - maxVisit: 'Daily maximum visits', - averageVisit: 'Daily average visits', - focusTitle: 'Browsing Time Trends', - visitTitle: 'Visit Trends', - } - }, - ja: { - common: { - focusTotal: '総閲覧時間', - visitTotal: '総訪問数', - ringGrowth: '前期比 {value}', - merged: '合并', - virtual: 'カスタマイズ', - hostPlaceholder: 'ドメイン名を検索', - emptyDesc: 'サイトは空です', - }, - summary: { - title: 'Summary', - day: 'Total active days', - firstDay: 'First visit {value}', - }, - trend: { - title: 'レンジトレンド', - startDate: '開始日', - endDate: '終了日', - lastWeek: '過去 7 日間', - last15Days: '過去 15 日間', - last30Days: '過去 30 日間', - last90Days: '過去 90 日間', - activeDay: 'アクティブな日', - totalDay: '間隔の合計日数', - maxFocus: '1 日の最大閲覧時間', - averageFocus: '1 日あたりの平均閲覧時間', - maxVisit: '1 日あたりの最大訪問数', - averageVisit: '1 日あたりの平均訪問数', - focusTitle: 'タイム トレンドの閲覧', - visitTitle: '訪問数の傾向', - } - } -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/confirm-resource.json b/src/i18n/message/app/confirm-resource.json new file mode 100644 index 000000000..ecd88c5bc --- /dev/null +++ b/src/i18n/message/app/confirm-resource.json @@ -0,0 +1,22 @@ +{ + "zh_CN": { + "confirmMsg": "好的", + "cancelMsg": "不用了" + }, + "zh_TW": { + "confirmMsg": "好的", + "cancelMsg": "不用了" + }, + "en": { + "confirmMsg": "OK", + "cancelMsg": "NO!" + }, + "ja": { + "confirmMsg": "OK", + "cancelMsg": "キャンセル" + }, + "pt_PT": { + "confirmMsg": "OK", + "cancelMsg": "Não!" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/confirm.ts b/src/i18n/message/app/confirm.ts index 956e81f86..3af780466 100644 --- a/src/i18n/message/app/confirm.ts +++ b/src/i18n/message/app/confirm.ts @@ -5,28 +5,13 @@ * https://opensource.org/licenses/MIT */ +import resource from './confirm-resource.json' + export type ConfirmMessage = { confirmMsg: string, cancelMsg: string } -const _default: Messages = { - zh_CN: { - confirmMsg: '好的', - cancelMsg: '不用了', - }, - zh_TW: { - confirmMsg: '好的', - cancelMsg: '不用了', - }, - en: { - confirmMsg: 'OK', - cancelMsg: 'NO!', - }, - ja: { - confirmMsg: 'OK', - cancelMsg: 'キャンセル', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/dashboard-resource.json b/src/i18n/message/app/dashboard-resource.json new file mode 100644 index 000000000..12c6539e2 --- /dev/null +++ b/src/i18n/message/app/dashboard-resource.json @@ -0,0 +1,132 @@ +{ + "zh_CN": { + "heatMap": { + "title0": "近一年上网总时长超过 {hour} 小时", + "title1": "近一年上网总时长不足 1 小时", + "tooltip0": "{year}/{month}/{day} 浏览网页 {minute} 分钟", + "tooltip1": "{year}/{month}/{day} 浏览网页 {hour} 小时 {minute} 分钟" + }, + "topK": { + "title": "近 {day} 天最常访问 TOP {k}", + "tooltip": "访问 {host} {visit} 次" + }, + "indicator": { + "installedDays": "已使用 {number} 天", + "visitCount": "累计访问过 {site} 个网站共 {visit} 次", + "browsingTime": "浏览时长超过 {minute} 分钟", + "mostUse": "最喜欢在 {start} 点至 {end} 点之间打开浏览器" + }, + "weekOnWeek": { + "title": "近一周浏览时长环比变化 TOP {k}", + "lastBrowse": "上周浏览 {time}", + "thisBrowse": "本周浏览 {time}", + "wow": "环比{state} {delta}", + "increase": "增长", + "decline": "减少" + } + }, + "zh_TW": { + "heatMap": { + "title0": "近一年上網總時長超過 {hour} 小時", + "title1": "近一年上網總時長不足 1 小時", + "tooltip0": "{year}/{month}/{day} 瀏覽網頁 {minute} 分鐘", + "tooltip1": "{year}/{month}/{day} 瀏覽網頁 {hour} 小時 {minute} 分鐘" + }, + "topK": { + "title": "近 {day} 天最常拜訪 TOP {k}", + "tooltip": "拜訪 {host} {visit} 次" + }, + "indicator": { + "installedDays": "已使用 {number} 天", + "visitCount": "拜訪過 {site} 個網站總計 {visit} 次", + "browsingTime": "瀏覽網頁超過 {minute} 分鐘", + "mostUse": "最喜歡在 {start} 點至 {end} 點之間上網" + }, + "weekOnWeek": { + "title": "近一周瀏覽時長環比變化 TOP {k}", + "lastBrowse": "上週瀏覽 {time}", + "thisBrowse": "本週瀏覽 {time}", + "wow": "環比{state} {delta}", + "increase": "增長", + "decline": "減少" + } + }, + "en": { + "heatMap": { + "title0": "Browsed over {hour} hours in the last year", + "title1": "Browsed less than 1 hour last year", + "tooltip0": "Browsed for {minute} minutes on {month}/{day}/{year}", + "tooltip1": "Browsed for {hour} hours {minute} minutes on {month}/{day}/{year}" + }, + "topK": { + "title": "TOP {k} most visited in the past {day} days", + "tooltip": "Visited {host} {visit} times" + }, + "indicator": { + "installedDays": "Installed for {number} days", + "visitCount": "Visited {site} websites {visit} times", + "browsingTime": "Browsed over {minute} minutes", + "mostUse": "Favorite browsing between {start} and {end} o'clock" + }, + "weekOnWeek": { + "title": "TOP {k} week-on-week change of browsing time", + "lastBrowse": "Browsed {time} last week", + "thisBrowse": "Browsed {time} this week", + "wow": "{delta} {state}", + "increase": "increased", + "decline": "decreased" + } + }, + "ja": { + "heatMap": { + "title0": "過去1年間に {hour} 時間以上オンラインで過ごした", + "title1": "過去 1 年間にオンラインで費やした時間は 1 時間未満", + "tooltip0": "{year} 年 {month} 月 {day} 日 {minute} 分間ウェブを閲覧する", + "tooltip1": "{year} 年 {month} 月 {day} 日 ウェブを {hour} 時間 {minute} 分閲覧する" + }, + "topK": { + "title": "過去 {day} 日間に最も拜訪された TOP {k}", + "tooltip": "{host} {visit} 回拜訪" + }, + "indicator": { + "installedDays": "使用 {number} 日", + "visitCount": "{site} つのサイトへの合計 {visit} 回の拜訪", + "browsingTime": "{minute} 分以上ウェブを閲覧する", + "mostUse": "{start}:00 から {end}:00 までのお気に入りのインターネットアクセス" + }, + "weekOnWeek": { + "title": "週ごとの変更 TOP {k}", + "lastBrowse": "先週 {time} 閲覧", + "thisBrowse": "今週は {time} で閲覧", + "wow": "毎週 {delta} の {state}", + "increase": "増加", + "decline": "減らす" + } + }, + "pt_PT": { + "heatMap": { + "title0": "Navegou mais de {hour} horas no último ano", + "title1": "Navegue por menos de 1 hora no ano passado", + "tooltip0": "Navegado por {minute} minutos em {day}/{month}/{year}", + "tooltip1": "Navegado por {hour} horas e {minute} minutos em {day}/{month}/{year}" + }, + "topK": { + "title": "TOP {k} mais visitados nos últimos {day} dias", + "tooltip": "Visitou {host} por {visit} vezes" + }, + "indicator": { + "installedDays": "Instalado por {number} dias", + "visitCount": "Visite {site} sites {visit} vezes", + "browsingTime": "Navegado por {minute} minutos", + "mostUse": "Navegação favorita entre {start} e {end} horas" + }, + "weekOnWeek": { + "title": "TOP {k} semana após semana mudança no tempo de navegação", + "lastBrowse": "Navegou {time} na última semana", + "thisBrowse": "Navegou {time} nesta semana", + "wow": "{delta} {state}", + "increase": "aumentou", + "decline": "diminuiu" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/app/dashboard.ts b/src/i18n/message/app/dashboard.ts index 6c47609f1..a58175227 100644 --- a/src/i18n/message/app/dashboard.ts +++ b/src/i18n/message/app/dashboard.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './dashboard-resource.json' + export type DashboardMessage = { heatMap: { title0: string @@ -32,111 +34,6 @@ export type DashboardMessage = { } } -const _default: Messages = { - zh_CN: { - heatMap: { - title0: '近一年上网总时长超过 {hour} 小时', - title1: '近一年上网总时长不足 1 小时', - tooltip0: '{year}/{month}/{day} 浏览网页 {minute} 分钟', - tooltip1: '{year}/{month}/{day} 浏览网页 {hour} 小时 {minute} 分钟', - }, - topK: { - title: '近 {day} 天最常访问 TOP {k}', - tooltip: '访问 {host} {visit} 次', - }, - indicator: { - installedDays: '已使用 {number} 天', - visitCount: '累计访问过 {site} 个网站共 {visit} 次', - browsingTime: '浏览时长超过 {minute} 分钟', - mostUse: '最喜欢在 {start} 点至 {end} 点之间打开浏览器', - }, - weekOnWeek: { - title: '近一周浏览时长环比变化 TOP {k}', - lastBrowse: '上周浏览 {time}', - thisBrowse: '本周浏览 {time}', - wow: '环比{state} {delta}', - increase: '增长', - decline: '减少', - }, - }, - zh_TW: { - heatMap: { - title0: '近一年上網總時長超過 {hour} 小時', - title1: '近一年上網總時長不足 1 小時', - tooltip0: '{year}/{month}/{day} 瀏覽網頁 {minute} 分鐘', - tooltip1: '{year}/{month}/{day} 瀏覽網頁 {hour} 小時 {minute} 分鐘', - }, - topK: { - title: '近 {day} 天最常拜訪 TOP {k}', - tooltip: '拜訪 {host} {visit} 次', - }, - indicator: { - installedDays: '已使用 {number} 天', - visitCount: '拜訪過 {site} 個網站總計 {visit} 次', - browsingTime: '瀏覽網頁超過 {minute} 分鐘', - mostUse: '最喜歡在 {start} 點至 {end} 點之間上網', - }, - weekOnWeek: { - title: '近一周瀏覽時長環比變化 TOP {k}', - lastBrowse: '上週瀏覽 {time}', - thisBrowse: '本週瀏覽 {time}', - wow: '環比{state} {delta}', - increase: '增長', - decline: '減少', - }, - }, - en: { - heatMap: { - title0: 'Browsed over {hour} hours in the last year', - title1: 'Browsed less than 1 hour last year', - tooltip0: 'Browsed for {minute} minutes on {month}/{day}/{year}', - tooltip1: 'Browsed for {hour} hours {minute} minutes on {month}/{day}/{year}', - }, - topK: { - title: 'TOP {k} most visited in the past {day} days', - tooltip: 'Visited {host} {visit} times', - }, - indicator: { - installedDays: 'Installed for {number} days', - visitCount: 'Visited {site} websites {visit} times', - browsingTime: 'Browsed over {minute} minutes', - mostUse: 'Favorite browsing between {start} and {end} o\'clock', - }, - weekOnWeek: { - title: 'TOP {k} week-on-week change of browsing time', - lastBrowse: 'Browsed {time} last week', - thisBrowse: 'Browsed {time} this week', - wow: '{delta} {state}', - increase: 'increased', - decline: 'decreased', - }, - }, - ja: { - heatMap: { - title0: '過去1年間に {hour} 時間以上オンラインで過ごした', - title1: '過去 1 年間にオンラインで費やした時間は 1 時間未満', - tooltip0: '{year} 年 {month} 月 {day} 日 {minute} 分間ウェブを閲覧する', - tooltip1: '{year} 年 {month} 月 {day} 日 ウェブを {hour} 時間 {minute} 分閲覧する', - }, - topK: { - title: '過去 {day} 日間に最も拜訪された TOP {k}', - tooltip: '{host} {visit} 回拜訪', - }, - indicator: { - installedDays: '使用 {number} 日', - visitCount: '{site} つのサイトへの合計 {visit} 回の拜訪', - browsingTime: '{minute} 分以上ウェブを閲覧する', - mostUse: '{start}:00 から {end}:00 までのお気に入りのインターネットアクセス', - }, - weekOnWeek: { - title: '週ごとの変更 TOP {k}', - lastBrowse: '先週 {time} 閲覧', - thisBrowse: '今週は {time} で閲覧', - wow: '毎週 {delta} の {state}', - increase: '増加', - decline: '減らす', - }, - }, -} +const _default: Messages = resource export default _default diff --git a/src/i18n/message/app/data-manage-resource.json b/src/i18n/message/app/data-manage-resource.json new file mode 100644 index 000000000..0de06be0e --- /dev/null +++ b/src/i18n/message/app/data-manage-resource.json @@ -0,0 +1,112 @@ +{ + "zh_CN": { + "totalMemoryAlert": "浏览器为每个扩展提供 {size}MB 来存储本地数据", + "totalMemoryAlert1": "无法确定浏览器允许的最大可用内存", + "usedMemoryAlert": "当前已使用 {size}MB", + "operationAlert": "您可以删除那些无关紧要的数据,来减小内存空间", + "filterItems": "数据筛选", + "filterFocus": "当日阅览时间在 {start} 秒至 {end} 秒之间。", + "filterTime": "当日打开次数在 {start} 次至 {end} 次之间。", + "filterDate": "{picker} 产生的数据。", + "unlimited": "无限", + "dateShortcut": { + "tillYesterday": "直到昨天", + "till7DaysAgo": "直到7天前", + "till30DaysAgo": "直到30天前" + }, + "paramError": "参数错误,请检查!", + "deleteConfirm": "共筛选出 {count} 条数据,是否全部删除?", + "deleteSuccess": "删除成功", + "migrationAlert": "使用导入/导出在不同浏览器之间迁移数据", + "importError": "文件格式错误", + "migrated": "成功导入" + }, + "zh_TW": { + "totalMemoryAlert": "瀏覽器爲每個擴充提供 {size}MB 來存儲本地數據", + "totalMemoryAlert1": "無法確定瀏覽器允許的最大可用內存", + "usedMemoryAlert": "當前已使用 {size}MB", + "operationAlert": "您可以刪除那些無關緊要的數據,來減小內存空間", + "filterItems": "數據篩選", + "filterFocus": "當日閱覽時間在 {start} 秒至 {end} 秒之間。", + "filterTime": "當日打開次數在 {start} 次至 {end} 次之間。", + "filterDate": "{picker} 産生的數據。", + "unlimited": "無限", + "dateShortcut": { + "tillYesterday": "直到昨天", + "till7DaysAgo": "直到7天前", + "till30DaysAgo": "直到30天前" + }, + "paramError": "參數錯誤,請檢查!", + "deleteConfirm": "共篩選出 {count} 條數據,是否全部刪除?", + "deleteSuccess": "刪除成功", + "migrationAlert": "使用導入/導出在不同瀏覽器之間遷移數據", + "importError": "文件格式錯誤", + "migrated": "成功導入" + }, + "en": { + "totalMemoryAlert": "The browser provides {size}MB to store local data for each extension", + "totalMemoryAlert1": "Unable to determine the maximum memory available allowed by the browser", + "usedMemoryAlert": "{size}MB is currently used", + "operationAlert": "You can delete those unimportant data to reduce memory usage", + "filterItems": "Filter data", + "filterFocus": "The browsing time of the day is between {start} seconds and {end} seconds", + "filterTime": "The number of visits for the day is between {start} and {end}", + "filterDate": "Recorded between {picker}", + "unlimited": "∞", + "dateShortcut": { + "tillYesterday": "Until yesterday", + "till7DaysAgo": "Until 7 days ago", + "till30DaysAgo": "Until 30 days ago" + }, + "paramError": "The parameter is wrong, please check!", + "deleteConfirm": "A total of {count} records have been filtered out. Do you want to delete them all?", + "deleteSuccess": "Deleted successfully!", + "migrationAlert": "Migrate data between browsers using import and export", + "importError": "Wrong file extension", + "migrated": "Imported successfully!" + }, + "ja": { + "totalMemoryAlert": "ブラウザは、データを保存するために各拡張機能に {size}MB のメモリを提供します", + "totalMemoryAlert1": "ブラウザで許可されている各拡張機能で使用可能な最大メモリを特定できません", + "usedMemoryAlert": "現在 {size}MB が使用されています", + "operationAlert": "これらの重要でないデータを削除して、メモリ使用量を減らすことができます", + "filterItems": "データフィルタリング", + "filterFocus": "当日の閲覧時間は、{start} 秒から {end} 秒の間です。", + "filterTime": "当日のオープン数は {start} から {end} の間です。", + "filterDate": "{picker} までに生成されたデータ", + "unlimited": "無制限", + "dateShortcut": { + "tillYesterday": "昨日まで", + "till7DaysAgo": "7日前まで", + "till30DaysAgo": "30日前まで" + }, + "paramError": "パラメータエラー、確認してください!", + "deleteConfirm": "合計 {count} 個のデータが除外されました。すべて削除しますか?", + "deleteSuccess": "正常に削除されました", + "migrationAlert": "インポート/エクスポートを使用して、異なるブラウザ間でデータを移行します", + "importError": "ファイル形式エラー", + "migrated": "正常にインポートされました" + }, + "pt_PT": { + "totalMemoryAlert": "O navegador fornece {size}MB para armazenar dados locais para cada extensão", + "totalMemoryAlert1": "Não é possível determinar a memória máxima disponível permitida pelo navegador", + "usedMemoryAlert": "{size}MB está atualmente em uso", + "operationAlert": "Você pode excluir esses dados sem importância para reduzir o uso de memória", + "filterItems": "Filtrar dados", + "filterFocus": "O tempo de navegação do dia está entre {start} segundos e {end} segundos", + "filterTime": "O número de visitas do dia está entre {start} e {end}", + "filterDate": "Gravado entre {picker}", + "unlimited": "∞", + "paramError": "O parâmetro está errado, por favor verifique!", + "deleteConfirm": "Um total de {count} registros foram filtrados. Deseja excluir todos eles?", + "deleteSuccess": "Eliminado com êxito!", + "migrationAlert": "Migrar dados entre navegadores usando a importação e exportação", + "importError": "Formato de arquivo incorreto", + "migrated": "Importado com sucesso!", + "dateShortcut": { + "tillYesterday": "Até ontem", + "till7DaysAgo": "Até 7 dias atrás", + "till30DaysAgo": "Até 30 dias atrás" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/app/data-manage.ts b/src/i18n/message/app/data-manage.ts index 91023047f..5c429d15b 100644 --- a/src/i18n/message/app/data-manage.ts +++ b/src/i18n/message/app/data-manage.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './data-manage-resource.json' + export type DataManageMessage = { totalMemoryAlert: string totalMemoryAlert1: string @@ -21,8 +23,6 @@ export type DataManageMessage = { migrationAlert: string importError: string migrated: string - operationCancel: string - operationConfirm: string dateShortcut: { tillYesterday: string till7DaysAgo: string @@ -30,103 +30,6 @@ export type DataManageMessage = { } } -const _default: Messages = { - zh_CN: { - totalMemoryAlert: '浏览器为每个扩展提供 {size}MB 来存储本地数据', - totalMemoryAlert1: '无法确定浏览器允许的最大可用内存', - usedMemoryAlert: '当前已使用 {size}MB', - operationAlert: '您可以删除那些无关紧要的数据,来减小内存空间', - filterItems: '数据筛选', - filterFocus: '当日阅览时间在 {start} 秒至 {end} 秒之间。', - filterTime: '当日打开次数在 {start} 次至 {end} 次之间。', - filterDate: '{picker} 产生的数据。', - unlimited: '无限', - dateShortcut: { - tillYesterday: '直到昨天', - till7DaysAgo: '直到7天前', - till30DaysAgo: '直到30天前', - }, - paramError: '参数错误,请检查!', - deleteConfirm: '共筛选出 {count} 条数据,是否全部删除?', - deleteSuccess: '删除成功', - migrationAlert: '使用导入/导出在不同浏览器之间迁移数据', - importError: '文件格式错误', - migrated: '成功导入', - operationCancel: '取消', - operationConfirm: '确认', - }, - zh_TW: { - totalMemoryAlert: '瀏覽器爲每個擴充提供 {size}MB 來存儲本地數據', - totalMemoryAlert1: '無法確定瀏覽器允許的最大可用內存', - usedMemoryAlert: '當前已使用 {size}MB', - operationAlert: '您可以刪除那些無關緊要的數據,來減小內存空間', - filterItems: '數據篩選', - filterFocus: '當日閱覽時間在 {start} 秒至 {end} 秒之間。', - filterTime: '當日打開次數在 {start} 次至 {end} 次之間。', - filterDate: '{picker} 産生的數據。', - unlimited: '無限', - dateShortcut: { - tillYesterday: '直到昨天', - till7DaysAgo: '直到7天前', - till30DaysAgo: '直到30天前', - }, - paramError: '參數錯誤,請檢查!', - deleteConfirm: '共篩選出 {count} 條數據,是否全部刪除?', - deleteSuccess: '刪除成功', - migrationAlert: '使用導入/導出在不同瀏覽器之間遷移數據', - importError: '文件格式錯誤', - migrated: '成功導入', - operationCancel: '取消', - operationConfirm: '確認', - }, - en: { - totalMemoryAlert: 'The browser provides {size}MB to store local data for each extension', - totalMemoryAlert1: 'Unable to determine the maximum memory available allowed by the browser', - usedMemoryAlert: '{size}MB is currently used', - operationAlert: 'You can delete those unimportant data to reduce memory usage', - filterItems: 'Filter data', - filterFocus: 'The browsing time of the day is between {start} seconds and {end} seconds', - filterTime: 'The number of visits for the day is between {start} and {end}', - filterDate: 'Recorded between {picker}', - unlimited: '∞', - dateShortcut: { - tillYesterday: 'Until yesterday', - till7DaysAgo: 'Until 7 days ago', - till30DaysAgo: 'Until 30 days ago', - }, - paramError: 'The parameter is wrong, please check!', - deleteConfirm: 'A total of {count} records have been filtered out. Do you want to delete them all?', - deleteSuccess: 'Deleted successfully!', - migrationAlert: 'Migrate data between browsers using import and export', - importError: 'Wrong file extension', - migrated: 'Imported successfully!', - operationCancel: 'Cancel', - operationConfirm: 'Confirm', - }, - ja: { - totalMemoryAlert: 'ブラウザは、データを保存するために各拡張機能に {size}MB のメモリを提供します', - totalMemoryAlert1: 'ブラウザで許可されている各拡張機能で使用可能な最大メモリを特定できません', - usedMemoryAlert: '現在 {size}MB が使用されています', - operationAlert: 'これらの重要でないデータを削除して、メモリ使用量を減らすことができます', - filterItems: 'データフィルタリング', - filterFocus: '当日の閲覧時間は、{start} 秒から {end} 秒の間です。', - filterTime: '当日のオープン数は {start} から {end} の間です。', - filterDate: '{picker} までに生成されたデータ', - unlimited: '無制限', - dateShortcut: { - tillYesterday: '昨日まで', - till7DaysAgo: '7日前まで', - till30DaysAgo: '30日前まで', - }, - paramError: 'パラメータエラー、確認してください!', - deleteConfirm: '合計 {count} 個のデータが除外されました。すべて削除しますか?', - deleteSuccess: '正常に削除されました', - migrationAlert: 'インポート/エクスポートを使用して、異なるブラウザ間でデータを移行します', - importError: 'ファイル形式エラー', - migrated: '正常にインポートされました', - operationCancel: '取消', - operationConfirm: '確認', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/habit-resource.json b/src/i18n/message/app/habit-resource.json new file mode 100644 index 000000000..371c723ad --- /dev/null +++ b/src/i18n/message/app/habit-resource.json @@ -0,0 +1,87 @@ +{ + "zh_CN": { + "sizes": { + "fifteen": "每十五分钟统计一次", + "halfHour": "每半小时统计一次", + "hour": "每一小时统计一次", + "twoHour": "每两小时统计一次" + }, + "average": { + "label": "平均每天" + }, + "chart": { + "title": "上网习惯统计", + "saveAsImageTitle": "保存", + "yAxisMin": "浏览时长 / 分钟", + "yAxisHour": "浏览时长 / 小时" + } + }, + "zh_TW": { + "sizes": { + "fifteen": "按十五分鐘統計", + "halfHour": "按半小時統計", + "hour": "按一小時統計", + "twoHour": "按兩小時統計" + }, + "average": { + "label": "平均每天" + }, + "chart": { + "title": "上網習慣統計", + "saveAsImageTitle": "保存", + "yAxisMin": "瀏覽時長 / 分鐘", + "yAxisHour": "瀏覽時長 / 小時" + } + }, + "en": { + "sizes": { + "fifteen": "Per 15 minutes", + "halfHour": "Per half hour", + "hour": "Per one hour", + "twoHour": "Per two hours" + }, + "average": { + "label": "Daily average" + }, + "chart": { + "title": "Time-phased Statistics of Browsing Time", + "saveAsImageTitle": "Snapshot", + "yAxisMin": "Browsing Time / minute", + "yAxisHour": "Browsing Time / hour" + } + }, + "ja": { + "sizes": { + "fifteen": "15分で統計", + "halfHour": "30分で統計", + "hour": "1時間ごとの統計", + "twoHour": "2時間ごとの統計" + }, + "average": { + "label": "1日平均" + }, + "chart": { + "title": "時系列の統計を閲覧する", + "saveAsImageTitle": "ダウンロード", + "yAxisMin": "閲覧時間/分", + "yAxisHour": "閲覧時間/時間" + } + }, + "pt_PT": { + "sizes": { + "fifteen": "Por 15 minutos", + "halfHour": "Por meia hora", + "hour": "Por 1 hora", + "twoHour": "Por 2 horas" + }, + "average": { + "label": "Média diária" + }, + "chart": { + "title": "Estatísticas do tempo de navegação", + "saveAsImageTitle": "Capturas", + "yAxisMin": "Tempo de Navegação / Minutos", + "yAxisHour": "Tempo de Navegação / Hora" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/app/habit.ts b/src/i18n/message/app/habit.ts index 45835e60d..5f3958a9a 100644 --- a/src/i18n/message/app/habit.ts +++ b/src/i18n/message/app/habit.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './habit-resource.json' + export type HabitMessage = { sizes: { fifteen: string @@ -15,14 +17,6 @@ export type HabitMessage = { average: { label: string }, - dateRange: { - lastDay: string - last3Days: string - lastWeek: string - last15Days: string - last30Days: string - last60Days: string - }, chart: { title: string saveAsImageTitle: string @@ -31,107 +25,6 @@ export type HabitMessage = { } } -const _default: Messages = { - zh_CN: { - sizes: { - fifteen: '每十五分钟统计一次', - halfHour: '每半小时统计一次', - hour: '每一小时统计一次', - twoHour: '每两小时统计一次', - }, - average: { - label: '平均每天', - }, - dateRange: { - lastDay: '最近 24 小时', - last3Days: '最近 3 天', - lastWeek: '最近 7 天', - last15Days: '最近 15 天', - last30Days: '最近 30 天', - last60Days: '最近 60 天', - }, - chart: { - title: '上网习惯统计', - saveAsImageTitle: '保存', - yAxisMin: '浏览时长 / 分钟', - yAxisHour: '浏览时长 / 小时', - }, - }, - zh_TW: { - sizes: { - fifteen: '按十五分鐘統計', - halfHour: '按半小時統計', - hour: '按一小時統計', - twoHour: '按兩小時統計', - }, - average: { - label: '平均每天', - }, - dateRange: { - lastDay: '最近 24 小時', - last3Days: '最近 3 天', - lastWeek: '最近 7 天', - last15Days: '最近 15 天', - last30Days: '最近 30 天', - last60Days: '最近 60 天', - }, - chart: { - title: '上網習慣統計', - saveAsImageTitle: '保存', - yAxisMin: '瀏覽時長 / 分鐘', - yAxisHour: '瀏覽時長 / 小時', - }, - }, - en: { - sizes: { - fifteen: 'Per 15 minutes', - halfHour: 'Per half hour', - hour: 'Per one hour', - twoHour: 'Per two hours', - }, - average: { - label: 'Daily average', - }, - dateRange: { - lastDay: 'Last day', - last3Days: 'Last 3 days', - lastWeek: 'Last week', - last15Days: 'Last 15 days', - last30Days: 'Last 30 days', - last60Days: 'Last 60 days', - }, - chart: { - title: 'Time-phased Statistics of Browsing Time', - saveAsImageTitle: 'Snapshot', - yAxisMin: 'Browsing Time / minute', - yAxisHour: 'Browsing Time / hour', - }, - }, - ja: { - sizes: { - fifteen: '15分で統計', - halfHour: '30分で統計', - hour: '1時間ごとの統計', - twoHour: '2時間ごとの統計', - }, - average: { - label: '1日平均', - }, - dateRange: { - lastDay: '過去24時間', - last3Days: '過去3日間', - lastWeek: '先週', - last15Days: '過去15日間', - last30Days: '過去30日間', - last60Days: '過去60日間', - }, - chart: { - title: '時系列の統計を閲覧する', - saveAsImageTitle: 'ダウンロード', - yAxisMin: '閲覧時間/分', - yAxisHour: '閲覧時間/時間', - }, - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/help-us-resource.json b/src/i18n/message/app/help-us-resource.json new file mode 100644 index 000000000..ccaa4a9dd --- /dev/null +++ b/src/i18n/message/app/help-us-resource.json @@ -0,0 +1,31 @@ +{ + "zh_CN": { + "title": "欢迎一起来改善本地化翻译!", + "alert": { + "l1": "由于作者的语言能力,该扩展原生只支持简体中文和英语,其他语言要么缺失,要么就严重依赖机器翻译。", + "l2": "为了能够提供更好的用户体验,我将其他语言的翻译任务托管在了 Crowdin 上。Crowdin 是一个对开源软件免费的翻译管理系统。", + "l3": "如果您觉得这个扩展对您有用,并且您愿意完善它的文本翻译的话,可以点击下方按钮前往 Crowdin 上的项目主页。", + "l4": "当某种语言的翻译进度达到 50% 之后,我将会考虑在扩展中支持它。" + }, + "button": "前往 Crowdin", + "loading": "正在查询翻译进度...", + "contributors": "贡献者名单" + }, + "en": { + "title": "Feel free to help improve the extension's localization translations!", + "alert": { + "l1": "Due to the author's language ability, the extension only supports Simplified Chinese and English natively, and other languages are either missing or rely heavily on machine translation.", + "l2": "In order to provide a better user experience, I host the translation tasks for other languages on Crowdin.Crowdin is a translation management system free for open source software.", + "l3": "If you find this extension useful to you and you are willing to improve its translation,you can click the button below to go to the project home page on Crowdin.", + "l4": "When the translation progress of a language reaches 50%, I will consider supporting it in this extension." + }, + "button": "Go Crowdin", + "loading": "Checking translation progress...", + "contributors": "Contributor List" + }, + "zh_TW": { + "button": "前往 Crowdin", + "loading": "正在檢查翻譯進度...", + "contributors": "貢獻者名單" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/help-us.ts b/src/i18n/message/app/help-us.ts index d6e6cc564..dc0851a85 100644 --- a/src/i18n/message/app/help-us.ts +++ b/src/i18n/message/app/help-us.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './help-us-resource.json' + type _AlertLine = | 'l1' | 'l2' @@ -16,35 +18,9 @@ export type HelpUsMessage = { alert: { [line in _AlertLine]: string } button: string loading: string + contributors: string } -const _default: Messages = { - zh_CN: { - title: '欢迎一起来改善本地化翻译!', - alert: { - l1: '由于作者的语言能力,该扩展原生只支持简体中文和英语,其他语言要么缺失,要么就严重依赖机器翻译。', - l2: '为了能够提供更好的用户体验,我将其他语言的翻译任务托管在了 Crowdin 上。Crowdin 是一个对开源软件免费的翻译管理系统。', - l3: '如果您觉得这个扩展对您有用,并且您愿意完善它的文本翻译的话,可以点击下方按钮前往 Crowdin 上的项目主页。', - l4: '当某种语言的翻译进度达到 50% 之后,我将会考虑在扩展中支持它。', - }, - button: '前往 Crowdin', - loading: '正在查询翻译进度...', - }, - en: { - title: 'Feel free to help improve the extension\'s localization translations!', - alert: { - l1: 'Due to the author\'s language ability, the extension only supports Simplified Chinese and English natively, and other languages are either missing or rely heavily on machine translation.', - l2: 'In order to provide a better user experience, I host the translation tasks for other languages on Crowdin.Crowdin is a translation management system free for open source software.', - l3: 'If you find this extension useful to you and you are willing to improve its translation,you can click the button below to go to the project home page on Crowdin.', - l4: 'When the translation progress of a language reaches 50%, I will consider supporting it in this extension.', - }, - button: 'Go Crowdin', - loading: 'Checking translation progress...', - }, - zh_TW: { - button: '前往 Crowdin', - loading: '正在檢查翻譯進度...', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/index.ts b/src/i18n/message/app/index.ts index 5a7e79566..2aaae5051 100644 --- a/src/i18n/message/app/index.ts +++ b/src/i18n/message/app/index.ts @@ -1,11 +1,12 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ import itemMessages, { ItemMessage } from "@i18n/message/common/item" +import mergeCommonMessages, { MergeCommonMessage } from "@i18n/message/common/merge" import dataManageMessages, { DataManageMessage } from "./data-manage" import reportMessages, { ReportMessage } from "./report" import analysisMessages, { AnalysisMessage } from "./analysis" @@ -20,13 +21,15 @@ import operationMessages, { OperationMessage } from './operation' import confirmMessages, { ConfirmMessage } from './confirm' import dashboardMessages, { DashboardMessage } from "./dashboard" import timeFormatMessages, { TimeFormatMessage } from "./time-format" +import helpUsMessages, { HelpUsMessage } from "./help-us" import calendarMessages, { CalendarMessage } from "@i18n/message/common/calendar" import popupDurationMessages, { PopupDurationMessage } from "@i18n/message/common/popup-duration" -import helpUsMessages, { HelpUsMessage } from "./help-us" +import buttonMessages, { ButtonMessage } from "@i18n/message/common/button" export type AppMessage = { dataManage: DataManageMessage item: ItemMessage + mergeCommon: MergeCommonMessage report: ReportMessage whitelist: WhitelistMessage mergeRule: MergeRuleMessage @@ -43,89 +46,44 @@ export type AppMessage = { timeFormat: TimeFormatMessage duration: PopupDurationMessage helpUs: HelpUsMessage + button: ButtonMessage +} + +const CHILD_MESSAGES: { [key in keyof AppMessage]: Messages } = { + dataManage: dataManageMessages, + item: itemMessages, + mergeCommon: mergeCommonMessages, + report: reportMessages, + whitelist: whitelistMessages, + mergeRule: mergeRuleMessages, + option: optionMessages, + analysis: analysisMessages, + menu: menuMessages, + habit: habitMessages, + limit: limitMessages, + siteManage: siteManageManages, + operation: operationMessages, + confirm: confirmMessages, + dashboard: dashboardMessages, + calendar: calendarMessages, + timeFormat: timeFormatMessages, + duration: popupDurationMessages, + helpUs: helpUsMessages, + button: buttonMessages, +} + +function appMessageOf(locale: timer.Locale): AppMessage { + const entries: [string, any][] = Object.entries(CHILD_MESSAGES).map(([key, val]) => ([key, val[locale]])) + const result = Object.fromEntries(entries) as AppMessage + return result } const _default: Messages = { - zh_CN: { - dataManage: dataManageMessages.zh_CN, - item: itemMessages.zh_CN, - report: reportMessages.zh_CN, - whitelist: whitelistMessages.zh_CN, - mergeRule: mergeRuleMessages.zh_CN, - option: optionMessages.zh_CN, - analysis: analysisMessages.zh_CN, - menu: menuMessages.zh_CN, - habit: habitMessages.zh_CN, - limit: limitMessages.zh_CN, - siteManage: siteManageManages.zh_CN, - operation: operationMessages.zh_CN, - confirm: confirmMessages.zh_CN, - dashboard: dashboardMessages.zh_CN, - calendar: calendarMessages.zh_CN, - timeFormat: timeFormatMessages.zh_CN, - duration: popupDurationMessages.zh_CN, - helpUs: helpUsMessages.zh_CN, - }, - zh_TW: { - dataManage: dataManageMessages.zh_TW, - item: itemMessages.zh_TW, - report: reportMessages.zh_TW, - whitelist: whitelistMessages.zh_TW, - mergeRule: mergeRuleMessages.zh_TW, - option: optionMessages.zh_TW, - analysis: analysisMessages.zh_TW, - menu: menuMessages.zh_TW, - habit: habitMessages.zh_TW, - limit: limitMessages.zh_TW, - siteManage: siteManageManages.zh_TW, - operation: operationMessages.zh_TW, - confirm: confirmMessages.zh_TW, - dashboard: dashboardMessages.zh_TW, - calendar: calendarMessages.zh_TW, - timeFormat: timeFormatMessages.zh_TW, - duration: popupDurationMessages.zh_TW, - helpUs: helpUsMessages.zh_TW, - }, - en: { - dataManage: dataManageMessages.en, - item: itemMessages.en, - report: reportMessages.en, - whitelist: whitelistMessages.en, - mergeRule: mergeRuleMessages.en, - option: optionMessages.en, - analysis: analysisMessages.en, - menu: menuMessages.en, - habit: habitMessages.en, - limit: limitMessages.en, - siteManage: siteManageManages.en, - operation: operationMessages.en, - confirm: confirmMessages.en, - dashboard: dashboardMessages.en, - calendar: calendarMessages.en, - timeFormat: timeFormatMessages.en, - duration: popupDurationMessages.en, - helpUs: helpUsMessages.en, - }, - ja: { - dataManage: dataManageMessages.ja, - item: itemMessages.ja, - report: reportMessages.ja, - whitelist: whitelistMessages.ja, - mergeRule: mergeRuleMessages.ja, - option: optionMessages.ja, - analysis: analysisMessages.ja, - menu: menuMessages.ja, - habit: habitMessages.ja, - limit: limitMessages.ja, - siteManage: siteManageManages.ja, - operation: operationMessages.ja, - confirm: confirmMessages.ja, - dashboard: dashboardMessages.ja, - calendar: calendarMessages.ja, - timeFormat: timeFormatMessages.ja, - duration: popupDurationMessages.ja, - helpUs: helpUsMessages.ja, - } + zh_CN: appMessageOf('zh_CN'), + zh_TW: appMessageOf('zh_TW'), + en: appMessageOf('en'), + ja: appMessageOf('ja'), + pt_PT: appMessageOf('pt_PT'), } export default _default \ No newline at end of file diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json new file mode 100644 index 000000000..767acbbfa --- /dev/null +++ b/src/i18n/message/app/limit-resource.json @@ -0,0 +1,207 @@ +{ + "zh_CN": { + "conditionFilter": "输入网址,然后回车", + "filterDisabled": "过滤无效规则", + "item": { + "condition": "限制网址", + "time": "每日限制时长", + "waste": "今日浏览时长", + "enabled": "是否有效", + "delayAllowed": "再看 5 分钟", + "delayAllowedInfo": "上网时间超过限制时,点击【再看 5 分钟】短暂延时。如果关闭该功能则不能延时。", + "operation": "操作" + }, + "button": { + "test": "网址测试", + "option": "全局设置" + }, + "addTitle": "新增限制", + "useWildcard": "是否使用通配符", + "message": { + "saved": "保存成功", + "noUrl": "未填写限制网址", + "noTime": "未填写每日限制时长", + "deleteConfirm": "是否删除限制:{cond}?", + "deleted": "删除成功", + "noPermissionFirefox": "请先在插件管理页[about:addons]开启该插件的粘贴板权限", + "inputTestUrl": "请先输入需要测试的网址链接", + "clickTestButton": "输入完成后请点击【{buttonText}】按钮", + "noRuleMatched": "该网址未命中任何规则", + "rulesMatched": "该网址命中以下规则:" + }, + "urlPlaceholder": "请直接粘贴网址 ➡️", + "verification": { + "inputTip": "该规则已超时,如要表更,请输入以下问题的答案:{prompt}", + "inputTip2": "该规则已超时,如要表更,请在下列输入框中原样输入:{answer}", + "pswInputTip": "该规则已超时,如要表更,所以请在下列输入框中输入您的解锁密码", + "incorrectPsw": "密码错误", + "incorrectAnswer": "回答错误", + "pi": "圆周率 π 的小数部分第 {startIndex} 位到第 {endIndex} 位的共 {digitCount} 位数字", + "confession": "一寸光一寸金,寸金难买寸光阴" + } + }, + "zh_TW": { + "conditionFilter": "輸入網址,然後回車", + "filterDisabled": "過濾無效規則", + "item": { + "condition": "限製網址", + "time": "每日限製時長", + "waste": "今日瀏覽時長", + "enabled": "是否有效", + "delayAllowed": "再看 5 分鐘", + "delayAllowedInfo": "上網時間超過限製時,點擊【再看 5 分鐘】短暫延時。如果關閉該功能則不能延時。", + "operation": "操作" + }, + "button": { + "test": "網址測試", + "option": "設置" + }, + "addTitle": "新增限製", + "useWildcard": "是否使用通配符", + "message": { + "saved": "保存成功", + "noUrl": "未填冩限製網址", + "noTime": "未填冩每日限製時長", + "deleteConfirm": "是否刪除限製:{cond}?", + "deleted": "刪除成功", + "noPermissionFirefox": "請先在插件管理頁【about:addons】開啟該插件的粘貼闆權限", + "inputTestUrl": "請先輸入需要測試的網址鏈接", + "clickTestButton": "輸入完成後請點擊【{buttonText}】按鈕", + "noRuleMatched": "該網址未命中任何規則", + "rulesMatched": "該網址命中以下規則:" + }, + "urlPlaceholder": "請直接粘貼網址 ➡️", + "verification": { + "inputTip": "該規則已超時,如要表更,請輸入以下問題的答案:{prompt}", + "inputTip2": "該規則已超時,如要表更,請在下列輸入框中原樣輸入:{answer}", + "pswInputTip": "該規則已超時,如要表更,所以請在下列輸入框中輸入您的解鎖密碼", + "incorrectPsw": "密碼錯誤", + "incorrectAnswer": "答案不正確", + "pi": "圓周率 π 的小數部分第 {startIndex} 位到第 {endIndex} 位的共 {digitCount} 位數字", + "confession": "一寸光一寸金,寸金難買寸光陰" + } + }, + "en": { + "conditionFilter": "URL", + "filterDisabled": "Only enabled", + "item": { + "condition": "Restricted URL", + "time": "Daily time limit", + "waste": "Browsed today", + "enabled": "Enabled", + "delayAllowed": "More 5 minutes", + "delayAllowedInfo": "If it times out, allow a temporary delay of 5 minutes", + "operation": "Operations" + }, + "button": { + "test": "Test URL", + "option": "Options" + }, + "addTitle": "New", + "useWildcard": "Whether to use wildcard", + "message": { + "saved": "Saved successfully", + "noUrl": "Unfilled limited URL", + "noTime": "Unfilled limited time per day", + "deleteConfirm": "Do you want to delete the rule of {cond}?", + "deleted": "Deleted successfully", + "noPermissionFirefox": "Please enable the clipboard permission of this addon on the management page (about:addons) first", + "inputTestUrl": "Please enter the URL link to be tested first", + "clickTestButton": "After inputting, please click the button ({buttonText})", + "noRuleMatched": "The URL does not hit any rules", + "rulesMatched": "The URL hits the following rules:" + }, + "urlPlaceholder": "Please paste the URL directly ➡️", + "verification": { + "inputTip": "This rule has already been triggered. To modify it, please enter the answer to the following prompt: {prompt}", + "inputTip2": "This rule has already been triggered. To modify it, please enter it as it is: {answer}", + "pswInputTip": "This rule has already been triggered. To modify it, please enter your unlock password", + "incorrectPsw": "Incorrect password", + "incorrectAnswer": "Incorrect answer", + "pi": "{digitCount} digits from {startIndex} to {endIndex} of the decimal part of π", + "confession": "Time is fleeting" + } + }, + "ja": { + "conditionFilter": "URL", + "filterDisabled": "有效", + "item": { + "condition": "制限 URL", + "waste": "今日の時間を閲覧する", + "time": "1日あたりの制限", + "enabled": "有效", + "delayAllowed": "さらに5分間閲覧する", + "delayAllowedInfo": "時間が経過した場合は、一時的に5分遅らせることができます", + "operation": "操作" + }, + "button": { + "test": "テストURL", + "option": "設定" + }, + "addTitle": "新增", + "useWildcard": "ワイルドカードを使用するかどうか", + "message": { + "noUrl": "埋められていない制限URL", + "noTime": "1日の制限時間を記入しない", + "saved": "正常に保存", + "deleteConfirm": "{cond} の制限を削除しますか?", + "deleted": "正常に削除", + "noPermissionFirefox": "最初にプラグイン管理ページでプラグインのペーストボード権限を有効にしてください", + "inputTestUrl": "最初にテストする URL リンクを入力してください", + "clickTestButton": "入力後、ボタン({buttonText})をクリックしてください", + "noRuleMatched": "URL がどのルールとも一致しません", + "rulesMatched": "URL は次のルールに一致します。" + }, + "urlPlaceholder": "URLを直接貼り付けてください➡️", + "verification": { + "inputTip": "このルールは既にトリガーされています。変更するには、次のプロンプトに対する回答を入力してください: {prompt}", + "inputTip2": "このルールは既にトリガーされています。変更するには以下のように入力してください: {answer}", + "pswInputTip": "このルールは既にトリガーされています。変更するにはロック解除パスワードを入力してください。", + "incorrectPsw": "間違ったパスワード", + "incorrectAnswer": "間違った回答", + "pi": "πの小数部の{digitCount} から {startIndex} までの {endIndex} 桁の数", + "confession": "人生とは今日一日のことである" + } + }, + "pt_PT": { + "conditionFilter": "URL", + "filterDisabled": "Apenas Habilitadas", + "addTitle": "Criar", + "useWildcard": "Se deseja usar caractere curinga", + "urlPlaceholder": "Cole a URL diretamente ➡️", + "item": { + "condition": "Restrito URL", + "time": "Limite de tempo diário", + "waste": "Tempo de navegação hoje", + "enabled": "Ativado", + "delayAllowed": "Mais 5 minutos", + "delayAllowedInfo": "Se expirar, permita um atraso temporário de 5 minutos", + "operation": "Operações" + }, + "button": { + "test": "Testar URL", + "option": "Opções" + }, + "message": { + "saved": "Guardado com sucesso", + "noUrl": "URL limitada não preenchida", + "noTime": "Tempo por dia não preenchido", + "deleteConfirm": "Deseja excluir a regra de {cond}?", + "deleted": "Apagado com sucesso", + "noPermissionFirefox": "Por favor, ative a permissão da área de transferência para esta extensão na página de gestão (about:addons) primeiro", + "inputTestUrl": "Por favor insira o link de URL para testar primeiro", + "clickTestButton": "Após a entrada, por favor, clique no botão ({buttonText})", + "noRuleMatched": "O URL não atinge nenhuma regra", + "rulesMatched": "A URL atinge as seguintes regras:" + }, + "verification": { + "inputTip": "Esta regra já foi acionada. Para modificá-la, por favor, digite a resposta para o seguinte promotor: {prompt}", + "inputTip2": "Esta regra já foi acionada. Para modificá-la, por favor insira-a como está: {answer}", + "pswInputTip": "Esta regra já foi acionada. Para modificá-la, digite a sua senha de desbloqueio", + "incorrectPsw": "Palavra-passe incorreta", + "incorrectAnswer": "Resposta incorreta", + "pi": "{digitCount} dígitos de {startIndex} a {endIndex} da parte decimal de π", + "confession": "Tempo é dinheiro" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/app/limit.ts b/src/i18n/message/app/limit.ts index 50a2fc8e4..fb165e0ab 100644 --- a/src/i18n/message/app/limit.ts +++ b/src/i18n/message/app/limit.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './limit-resource.json' + export type LimitMessage = { conditionFilter: string filterDisabled: string @@ -21,13 +23,8 @@ export type LimitMessage = { operation: string } button: { - add: string test: string - testSimple: string - paste: string - save: string - delete: string - modify: string + option: string } message: { noUrl: string @@ -41,162 +38,17 @@ export type LimitMessage = { noRuleMatched: string rulesMatched: string } - testUrlLabel: string + verification: { + inputTip: string + inputTip2: string + pswInputTip: string + incorrectPsw: string + incorrectAnswer: string + pi: string + confession: string + } } -const _default: Messages = { - zh_CN: { - conditionFilter: '输入网址,然后回车', - filterDisabled: '过滤无效规则', - item: { - condition: '限制网址', - time: '每日限制时长', - waste: '今日浏览时长', - enabled: '是否有效', - delayAllowed: '再看 5 分钟', - delayAllowedInfo: '上网时间超过限制时,点击【再看 5 分钟】短暂延时。如果关闭该功能则不能延时。', - operation: '操作', - }, - button: { - add: '新增', - test: '网址测试', - testSimple: '测试', - paste: '粘贴', - save: '保存', - delete: '删除', - modify: '修改', - }, - addTitle: '新增限制', - useWildcard: '是否使用通配符', - message: { - saved: '保存成功', - noUrl: '未填写限制网址', - noTime: '未填写每日限制时长', - deleteConfirm: '是否删除限制:{cond}?', - deleted: '删除成功', - noPermissionFirefox: '请先在插件管理页[about:addons]开启该插件的粘贴板权限', - inputTestUrl: '请先输入需要测试的网址链接', - clickTestButton: '输入完成后请点击【{buttonText}】按钮', - noRuleMatched: '该网址未命中任何规则', - rulesMatched: '该网址命中以下规则:', - }, - testUrlLabel: '测试网址', - urlPlaceholder: '请直接粘贴网址 ➡️', - }, - zh_TW: { - conditionFilter: '輸入網址,然後回車', - filterDisabled: '過濾無效規則', - item: { - condition: '限製網址', - time: '每日限製時長', - waste: '今日瀏覽時長', - enabled: '是否有效', - delayAllowed: '再看 5 分鐘', - delayAllowedInfo: '上網時間超過限製時,點擊【再看 5 分鐘】短暫延時。如果關閉該功能則不能延時。', - operation: '操作', - }, - button: { - add: '新增', - test: '網址測試', - testSimple: '測試', - paste: '粘貼', - save: '保存', - delete: '刪除', - modify: '修改', - }, - addTitle: '新增限製', - useWildcard: '是否使用通配符', - message: { - saved: '保存成功', - noUrl: '未填冩限製網址', - noTime: '未填冩每日限製時長', - deleteConfirm: '是否刪除限製:{cond}?', - deleted: '刪除成功', - noPermissionFirefox: '請先在插件管理頁[about:addons]開啟該插件的粘貼闆權限', - inputTestUrl: '請先輸入需要測試的網址鏈接', - clickTestButton: '輸入完成後請點擊【{buttonText}】按鈕', - noRuleMatched: '該網址未命中任何規則', - rulesMatched: '該網址命中以下規則:', - }, - urlPlaceholder: '請直接粘貼網址 ➡️', - testUrlLabel: '測試網址', - }, - en: { - conditionFilter: 'URL', - filterDisabled: 'Only enabled', - item: { - condition: 'Restricted URL', - time: 'Daily time limit', - waste: 'Browsed today', - enabled: 'Enabled', - delayAllowed: 'More 5 minutes', - delayAllowedInfo: 'If it times out, allow a temporary delay of 5 minutes', - operation: 'Operations', - }, - button: { - add: 'New', - test: 'Test URL', - testSimple: 'Test', - paste: 'Paste', - save: 'Save', - delete: 'Delete', - modify: 'Modify', - }, - addTitle: 'New', - useWildcard: 'Whether to use wildcard', - message: { - saved: 'Saved successfully', - noUrl: 'Unfilled limited URL', - noTime: 'Unfilled limited time per day', - deleteConfirm: 'Do you want to delete the rule of {cond}?', - deleted: 'Deleted successfully', - noPermissionFirefox: 'Please enable the clipboard permission of this addon on the management page (about:addons) first', - inputTestUrl: 'Please enter the URL link to be tested first', - clickTestButton: 'After inputting, please click the button ({buttonText})', - noRuleMatched: 'The URL does not hit any rules', - rulesMatched: 'The URL hits the following rules:', - }, - urlPlaceholder: 'Please paste the URL directly ➡️', - testUrlLabel: 'Test URL', - }, - ja: { - conditionFilter: 'URL', - filterDisabled: '有效', - item: { - condition: '制限 URL', - waste: '今日の時間を閲覧する', - time: '1日あたりの制限', - enabled: '有效', - delayAllowed: 'さらに5分間閲覧する', - delayAllowedInfo: '時間が経過した場合は、一時的に5分遅らせることができます', - operation: '操作', - }, - button: { - add: '新增', - test: 'テストURL', - testSimple: 'テスト', - paste: 'ペースト', - save: 'セーブ', - delete: '削除', - modify: '変更', - }, - addTitle: '新增', - useWildcard: 'ワイルドカードを使用するかどうか', - message: { - noUrl: '埋められていない制限URL', - noTime: '1日の制限時間を記入しない', - saved: '正常に保存', - deleteConfirm: '{cond} の制限を削除しますか?', - deleted: '正常に削除', - noPermissionFirefox: '最初にプラグイン管理ページでプラグインのペーストボード権限を有効にしてください', - inputTestUrl: '最初にテストする URL リンクを入力してください', - clickTestButton: '入力後、ボタン({buttonText})をクリックしてください', - noRuleMatched: 'URL がどのルールとも一致しません', - rulesMatched: 'URL は次のルールに一致します。', - }, - urlPlaceholder: 'URLを直接貼り付けてください➡️', - testUrlLabel: 'テスト URL', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/menu-resource.json b/src/i18n/message/app/menu-resource.json new file mode 100644 index 000000000..b27b9bf38 --- /dev/null +++ b/src/i18n/message/app/menu-resource.json @@ -0,0 +1,102 @@ +{ + "zh_CN": { + "dashboard": "仪表盘", + "data": "我的数据", + "dataReport": "报表明细", + "siteAnalysis": "站点分析", + "dataClear": "内存管理", + "additional": "附加功能", + "siteManage": "网站管理", + "whitelist": "白名单管理", + "mergeRule": "子域名合并", + "option": "扩展选项", + "behavior": "上网行为", + "habit": "上网习惯", + "limit": "每日时限设置", + "other": "其他", + "feedback": "有什么反馈吗?", + "rate": "打个分吧!", + "helpUs": "帮助我们~", + "userManual": "用户手册" + }, + "zh_TW": { + "dashboard": "儀錶盤", + "data": "我的數據", + "dataReport": "報表明細", + "siteAnalysis": "站點分析", + "dataClear": "內存管理", + "additional": "附加功能", + "siteManage": "網站管理", + "whitelist": "白名單管理", + "mergeRule": "子域名合並", + "option": "擴充選項", + "behavior": "上網行爲", + "habit": "上網習慣", + "limit": "每日時限設置", + "other": "其他", + "feedback": "有什麼反饋嗎?", + "rate": "打個分吧!", + "helpUs": "帮助我们~", + "userManual": "使用者手冊" + }, + "en": { + "dashboard": "Dashboard", + "data": "My Data", + "dataReport": "Record", + "siteAnalysis": "Site Analysis", + "dataClear": "Memory Situation", + "behavior": "User Behavior", + "habit": "Habits", + "limit": "Daily Limit", + "additional": "Additional Features", + "siteManage": "Site Management", + "whitelist": "Whitelist", + "mergeRule": "Merge-site Rules", + "other": "Other Features", + "option": "Options", + "feedback": "Feedback Questionnaire", + "rate": "Rate It", + "helpUs": "Help Us", + "userManual": "User Manual" + }, + "ja": { + "dashboard": "ダッシュボード", + "data": "私のデータ", + "dataReport": "報告する", + "siteAnalysis": "ウェブサイト分析", + "dataClear": "記憶状況", + "behavior": "ユーザーの行動", + "habit": "閲覧の習慣", + "limit": "閲覧の制限", + "additional": "その他の機能", + "siteManage": "ウェブサイト管理", + "whitelist": "Webホワイトリスト", + "mergeRule": "ドメイン合併", + "other": "その他の機能", + "option": "拡張設定", + "feedback": "フィードバックアンケート", + "rate": "それを評価", + "helpUs": "協力する", + "userManual": "ユーザーマニュアル" + }, + "pt_PT": { + "dashboard": "Painel de Controlo", + "data": "Os Meus Dados", + "dataReport": "Registo", + "dataClear": "Situação da Memória", + "behavior": "Comportamento do Utilizador", + "habit": "Hábitos", + "limit": "Limite diário", + "additional": "Características Adicionais", + "siteManage": "Gestão do Site", + "whitelist": "Lista Ignorada", + "mergeRule": "Regras para Mesclar Site", + "other": "Outras Características", + "option": "Opções", + "feedback": "Questionário de Opinião", + "rate": "Avalie", + "helpUs": "Ajude-nos", + "siteAnalysis": "Análise do Site", + "userManual": "Manual do Utilizador" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/menu.ts b/src/i18n/message/app/menu.ts index aee5d5e74..bdf518c5f 100644 --- a/src/i18n/message/app/menu.ts +++ b/src/i18n/message/app/menu.ts @@ -1,10 +1,12 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import resource from './menu-resource.json' + export type MenuMessage = { dashboard: string data: string @@ -26,87 +28,6 @@ export type MenuMessage = { userManual: string } -const _default: Messages = { - zh_CN: { - dashboard: '仪表盘', - data: '我的数据', - dataReport: '报表明细', - siteAnalysis: '站点分析', - dataClear: '内存管理', - additional: '附加功能', - siteManage: '网站管理', - whitelist: '白名单管理', - mergeRule: '子域名合并', - option: '扩展选项', - behavior: '上网行为', - habit: '上网习惯', - limit: '每日时限设置', - other: '其他', - feedback: '有什么反馈吗?', - rate: '打个分吧!', - helpUs: '帮助我们~', - userManual: '用户手册', - }, - zh_TW: { - dashboard: '儀錶盤', - data: '我的數據', - dataReport: '報表明細', - siteAnalysis: '站點分析', - dataClear: '內存管理', - additional: '附加功能', - siteManage: '網站管理', - whitelist: '白名單管理', - mergeRule: '子域名合並', - option: '擴充選項', - behavior: '上網行爲', - habit: '上網習慣', - limit: '每日時限設置', - other: '其他', - feedback: '有什麼反饋嗎?', - rate: '打個分吧!', - helpUs: '帮助我们~', - userManual: '用戶手冊', - }, - en: { - dashboard: 'Dashboard', - data: 'My Data', - dataReport: 'Record', - siteAnalysis: 'Site Analysis', - dataClear: 'Memory Situation', - behavior: 'User Behavior', - habit: 'Habits', - limit: 'Daily Limit', - additional: 'Additional Features', - siteManage: 'Site Management', - whitelist: 'Whitelist', - mergeRule: 'Merge-site Rules', - other: 'Other Features', - option: 'Options', - feedback: 'Feedback Questionnaire', - rate: 'Rate It', - helpUs: 'Help Us', - userManual: 'User Manual', - }, - ja: { - dashboard: 'ダッシュボード', - data: '私のデータ', - dataReport: '報告する', - siteAnalysis: 'ウェブサイト分析', - dataClear: '記憶状況', - behavior: 'ユーザーの行動', - habit: '閲覧の習慣', - limit: '閲覧の制限', - additional: 'その他の機能', - siteManage: 'ウェブサイト管理', - whitelist: 'Webホワイトリスト', - mergeRule: 'ドメイン合併', - other: 'その他の機能', - option: '拡張設定', - feedback: 'フィードバックアンケート', - rate: 'それを評価', - helpUs: '協力する', - userManual: 'ユーザーマニュアル', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/merge-rule-resource.json b/src/i18n/message/app/merge-rule-resource.json new file mode 100644 index 000000000..6ba2fa4f8 --- /dev/null +++ b/src/i18n/message/app/merge-rule-resource.json @@ -0,0 +1,77 @@ +{ + "zh_CN": { + "removeConfirmMsg": "自定义合并规则 {origin} 将被移除", + "originPlaceholder": "原域名", + "mergedPlaceholder": "合并后域名", + "errorOrigin": "原域名格式错误", + "duplicateMsg": "合并规则已存在:{origin}", + "addConfirmMsg": "将为 {origin} 设置自定义合并规则", + "infoAlertTitle": "该页面可以配置子域名的合并规则", + "infoAlert0": "点击新增按钮,会弹出原域名和合并后域名的输入框,填写并保存规则", + "infoAlert1": "原域名可填具体的域名或者正则表达式,比如 www.baidu.com,*.baidu.com,*.google.com.*。以此确定哪些域名在合并时会使用该条规则", + "infoAlert2": "合并后域名可填具体的域名,或者填数字,或者不填", + "infoAlert3": "如果填数字,则表示合并后域名的级数。比如存在规则【 *.*.edu.cn >>> 3 】,那么 www.hust.edu.cn 将被合并至 hust.edu.cn", + "infoAlert4": "如果不填,则表示原域名不会被合并", + "infoAlert5": "如果没有命中任何规则,则默认会合并至 {psl} 的前一级" + }, + "zh_TW": { + "removeConfirmMsg": "自定義合並規則 {origin} 將被移除", + "originPlaceholder": "原網域", + "mergedPlaceholder": "合並後網域", + "errorOrigin": "原網域格式錯誤", + "duplicateMsg": "合並規則已存在:{origin}", + "addConfirmMsg": "將爲 {origin} 設置自定義合並規則", + "infoAlertTitle": "該頁麵可以配置子網域的合並規則", + "infoAlert0": "點擊新增按鈕,會彈出原網域和合並後網域的輸入框,填冩並保存規則", + "infoAlert1": "原網域可填具體的網域或者正規表示式,比如 www.baidu.com,*.baidu.com,*.google.com.*。以此確定哪些網域在合並時會使用該條規則", + "infoAlert2": "合並後網域可填具體的網域,或者填數字,或者不填", + "infoAlert3": "如果填數字,則表示合並後網域的級數。比如存在規則【 *.*.edu.cn >>> 3 】,那麼 www.hust.edu.cn 將被合並至 hust.edu.cn", + "infoAlert4": "如果不填,則表示原網域不會被合並", + "infoAlert5": "如果沒有匹配任何規則,則默認會合並至 {psl} 的前一級" + }, + "en": { + "removeConfirmMsg": "{origin} will be removed from customized merge rules.", + "originPlaceholder": "Original site", + "mergedPlaceholder": "Merged", + "errorOrigin": "The format of original site is invalid.", + "duplicateMsg": "The rule already exists: {origin}", + "addConfirmMsg": "Customized merge rules will be set for {origin}", + "infoAlertTitle": "You can set the merge rules when counting sites on this page", + "infoAlert0": "Click the [New One] button, the input boxes of the source site and the merge site will be displayed, fill in and save the rule", + "infoAlert1": "The original site can be filled with a specific site or regular expression, such as www.baidu.com, *.baidu.com, *.google.com.*, to determine which sites will match this rule while merging", + "infoAlert2": "The merged site can be filled with a specific site, a number or blank", + "infoAlert3": "A number means the level of merged site. For example, there is a rule '*.*.edu.cn >>> 3', then 'www.hust.edu.cn' will be merged to 'hust.edu.cn'", + "infoAlert4": "Blank means the original site will not be merged", + "infoAlert5": "If no rule is matched, it will default to the level before {psl}" + }, + "ja": { + "removeConfirmMsg": "カスタム マージ ルール {origin} は削除されます", + "originPlaceholder": "独自ドメイン名", + "mergedPlaceholder": "統計的ドメイン名", + "errorOrigin": "元のドメイン名の形式が間違っています", + "duplicateMsg": "ルールはすでに存在します:{origin}", + "addConfirmMsg": "カスタム マージ ルールが {origin} に設定されます", + "infoAlertTitle": "このページでは、サブドメインのマージ ルールを設定できます", + "infoAlert0": "[追加] ボタンをクリックすると、元のドメイン名と結合されたドメイン名の入力ボックスがポップアップし、ルールを入力して保存します。", + "infoAlert1": "元のドメイン名には、特定のドメイン名または正規表現 (www.baidu.com、*.baidu.com、*.google.com.* など) を入力できます。 マージ時にこのルールを使用するドメインを決定するには", + "infoAlert2": "統合されたドメイン名の後、特定のドメイン名を入力するか、番号を入力するか、空白のままにすることができます", + "infoAlert3": "数字を記入する場合は、ドメイン名のレベルが予約されていることを意味します。 たとえば、ルール [*.*.edu.cn >>> 3 ] がある場合、www.hust.edu.cn は hust.edu.cn にマージされます。", + "infoAlert4": "記入しない場合は、元のドメイン名が統合されないことを意味します", + "infoAlert5": "一致するルールがない場合、デフォルトで {psl} より前のレベルになります" + }, + "pt_PT": { + "removeConfirmMsg": "{origin} será removido das regras personalizadas de mesclagem.", + "originPlaceholder": "Site original", + "mergedPlaceholder": "Mesclado", + "errorOrigin": "O formato do site original é inválido.", + "duplicateMsg": "A regra já existe: {origin}", + "addConfirmMsg": "Regras de mesclagem personalizadas serão definidas para {origin}", + "infoAlertTitle": "Você pode definir as regras de mesclagem ao contar sites nesta página", + "infoAlert0": "Clique no botão [Criar], as caixas de entrada do site de origem e o site de mesclagem serão exibidas, preencherão e salvarão a regra", + "infoAlert1": "O site original pode ser preenchido com um site específico ou expressão regular, como www.baidu.com, *.baidu.com, *.google.com.*, para determinar quais sites corresponderão a essa regra ao mesclar", + "infoAlert2": "O site mesclado pode ser preenchido com um site específico, um número ou em branco", + "infoAlert3": "Um número significa o nível de um site unificado. Por exemplo, há uma regra '*.*.edu.cn >>> 3', em seguida, 'www.hust.edu.cn' será fundida com 'hust.edu.cn'", + "infoAlert4": "Em branco significa que o site original não será mesclado", + "infoAlert5": "Se nenhuma regra for correspondida, o padrão será o nível antes de {psl}" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/merge-rule.ts b/src/i18n/message/app/merge-rule.ts index 87a91092f..ab5969afe 100644 --- a/src/i18n/message/app/merge-rule.ts +++ b/src/i18n/message/app/merge-rule.ts @@ -1,13 +1,13 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import resource from './merge-rule-resource.json' + export type MergeRuleMessage = { - resultOfOrigin: string - resultOfLevel: string removeConfirmMsg: string originPlaceholder: string mergedPlaceholder: string @@ -23,75 +23,6 @@ export type MergeRuleMessage = { infoAlert5: string } -const _default: Messages = { - zh_CN: { - resultOfOrigin: '不合并', - resultOfLevel: '{level} 级域名', - removeConfirmMsg: '自定义合并规则 {origin} 将被移除', - originPlaceholder: '原域名', - mergedPlaceholder: '合并后域名', - errorOrigin: '原域名格式错误', - duplicateMsg: '合并规则已存在:{origin}', - addConfirmMsg: '将为 {origin} 设置自定义合并规则', - infoAlertTitle: '该页面可以配置子域名的合并规则', - infoAlert0: '点击新增按钮,会弹出原域名和合并后域名的输入框,填写并保存规则', - infoAlert1: '原域名可填具体的域名或者正则表达式,比如 www.baidu.com,*.baidu.com,*.google.com.*。以此确定哪些域名在合并时会使用该条规则', - infoAlert2: '合并后域名可填具体的域名,或者填数字,或者不填', - infoAlert3: '如果填数字,则表示合并后域名的级数。比如存在规则【 *.*.edu.cn >>> 3 】,那么 www.hust.edu.cn 将被合并至 hust.edu.cn', - infoAlert4: '如果不填,则表示原域名不会被合并', - infoAlert5: '如果没有命中任何规则,则默认会合并至 {psl} 的前一级', - }, - zh_TW: { - resultOfOrigin: '不合並', - resultOfLevel: '{level} 級網域', - removeConfirmMsg: '自定義合並規則 {origin} 將被移除', - originPlaceholder: '原網域', - mergedPlaceholder: '合並後網域', - errorOrigin: '原網域格式錯誤', - duplicateMsg: '合並規則已存在:{origin}', - addConfirmMsg: '將爲 {origin} 設置自定義合並規則', - infoAlertTitle: '該頁麵可以配置子網域的合並規則', - infoAlert0: '點擊新增按鈕,會彈出原網域和合並後網域的輸入框,填冩並保存規則', - infoAlert1: '原網域可填具體的網域或者正則表達式,比如 www.baidu.com,*.baidu.com,*.google.com.*。以此確定哪些網域在合並時會使用該條規則', - infoAlert2: '合並後網域可填具體的網域,或者填數字,或者不填', - infoAlert3: '如果填數字,則表示合並後網域的級數。比如存在規則【 *.*.edu.cn >>> 3 】,那麼 www.hust.edu.cn 將被合並至 hust.edu.cn', - infoAlert4: '如果不填,則表示原網域不會被合並', - infoAlert5: '如果沒有匹配任何規則,則默認會合並至 {psl} 的前一級', - }, - en: { - resultOfOrigin: 'Not Merge', - resultOfLevel: 'Keep Level {level}', - removeConfirmMsg: '{origin} will be removed from customized merge rules.', - originPlaceholder: 'Origin site', - mergedPlaceholder: 'Merged', - errorOrigin: 'The format of origin site is invalid.', - duplicateMsg: 'The rule already exists: {origin}', - addConfirmMsg: 'Customized merge rules will be set for {origin}', - infoAlertTitle: 'You can set the merging rules when counting sites on this page', - infoAlert0: 'Click the [New One] button, the input boxes of the source site and the merge site will be displayed, fill in and save the rule', - infoAlert1: 'The origin site can be filled with a specific site or regular expression, such as www.baidu.com, *.baidu.com, *.google.com.*, to determine which sites will match this rule while merging', - infoAlert2: 'The merged site can be filled with a specific site, a number or blank', - infoAlert3: 'A number means the level of merged site. For example, there is a rule \'*.*.edu.cn >>> 3\', then \'www.hust.edu.cn\' will be merged to \'hust.edu.cn\'', - infoAlert4: 'Blank means the origin site will not be merged', - infoAlert5: 'If no rule is matched, it will default to the level before {psl}', - }, - ja: { - resultOfOrigin: '不合并', - resultOfLevel: '{level} 次ドメイン', - removeConfirmMsg: 'カスタム マージ ルール {origin} は削除されます', - originPlaceholder: '独自ドメイン名', - mergedPlaceholder: '統計的ドメイン名', - errorOrigin: '元のドメイン名の形式が間違っています', - duplicateMsg: 'ルールはすでに存在します:{origin}', - addConfirmMsg: 'カスタム マージ ルールが {origin} に設定されます', - infoAlertTitle: 'このページでは、サブドメインのマージ ルールを設定できます', - infoAlert0: '[追加] ボタンをクリックすると、元のドメイン名と結合されたドメイン名の入力ボックスがポップアップし、ルールを入力して保存します。', - infoAlert1: '元のドメイン名には、特定のドメイン名または正規表現 (www.baidu.com、*.baidu.com、*.google.com.* など) を入力できます。 マージ時にこのルールを使用するドメインを決定するには', - infoAlert2: '統合されたドメイン名の後、特定のドメイン名を入力するか、番号を入力するか、空白のままにすることができます', - infoAlert3: '数字を記入する場合は、ドメイン名のレベルが予約されていることを意味します。 たとえば、ルール [*.*.edu.cn >>> 3 ] がある場合、www.hust.edu.cn は hust.edu.cn にマージされます。', - infoAlert4: '記入しない場合は、元のドメイン名が統合されないことを意味します', - infoAlert5: '一致するルールがない場合、デフォルトで {psl} より前のレベルになります', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/operation-resource.json b/src/i18n/message/app/operation-resource.json new file mode 100644 index 000000000..03757ace3 --- /dev/null +++ b/src/i18n/message/app/operation-resource.json @@ -0,0 +1,22 @@ +{ + "zh_CN": { + "confirmTitle": "操作确认", + "successMsg": "操作成功!" + }, + "zh_TW": { + "confirmTitle": "操作確認", + "successMsg": "操作成功!" + }, + "en": { + "confirmTitle": "Confirmation", + "successMsg": "Successfully!" + }, + "ja": { + "confirmTitle": "動作確認", + "successMsg": "正常に動作しました!" + }, + "pt_PT": { + "confirmTitle": "Confirmação de Ação", + "successMsg": "Com êxito!" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/operation.ts b/src/i18n/message/app/operation.ts index 7eea0c7d5..bf3f18de4 100644 --- a/src/i18n/message/app/operation.ts +++ b/src/i18n/message/app/operation.ts @@ -5,38 +5,13 @@ * https://opensource.org/licenses/MIT */ +import resource from './operation-resource.json' + export type OperationMessage = { confirmTitle: string successMsg: string - save: string - newOne: string } -const _default: Messages = { - zh_CN: { - confirmTitle: '操作确认', - successMsg: '操作成功!', - newOne: '新增', - save: '保存', - }, - zh_TW: { - confirmTitle: '操作確認', - successMsg: '操作成功!', - newOne: '新增', - save: '保存', - }, - en: { - confirmTitle: 'Confirm', - successMsg: 'Successfully!', - newOne: 'New One', - save: 'Save', - }, - ja: { - confirmTitle: '動作確認', - successMsg: '正常に動作しました!', - newOne: '追加', - save: '保存', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json new file mode 100644 index 000000000..0b1193090 --- /dev/null +++ b/src/i18n/message/app/option-resource.json @@ -0,0 +1,496 @@ +{ + "zh_CN": { + "yes": "是", + "no": "否", + "popup": { + "title": "弹窗页", + "max": "只显示前 {input} 条数据,剩下的条目合并显示", + "defaultMergeDomain": "{input} 打开时合并子域名", + "defaultDisplay": "打开时显示 {duration} {type}", + "displaySiteName": "{input} 显示时是否使用 {siteName} 来代替域名", + "weekStart": "每周的第一天 {input}", + "weekStartAsNormal": "按照惯例" + }, + "appearance": { + "title": "外观", + "displayWhitelist": "{input} 是否在 {contextMenu} 里,显示 {whitelist} 相关功能", + "whitelistItem": "白名单", + "contextMenu": "浏览器的右键菜单", + "displayBadgeText": "{input} 是否在 {icon} 上,显示 {timeInfo}", + "icon": "扩展图标", + "badgeTextContent": "当前网站的今日浏览时长", + "locale": { + "label": "语言设置 {input}", + "default": "跟随浏览器", + "changeConfirm": "语言设置成功,请刷新页面!", + "reloadButton": "刷新" + }, + "printInConsole": { + "label": "{input} 是否在 {console} 里打印当前网站的 {info}", + "console": "浏览器的控制台", + "info": "今日访问信息" + }, + "darkMode": { + "label": "夜间模式 {input}", + "options": { + "default": "跟随浏览器", + "on": "始终开启", + "off": "始终关闭", + "timed": "定时开启" + } + } + }, + "statistics": { + "title": "统计", + "countWhenIdle": "{input} 是否统计 {idleTime} {info}", + "idleTime": "休眠时间", + "idleTimeInfo": "长时间不操作(比如全屏观看视频),浏览器会自动进入休眠状态", + "countLocalFiles": "{input} 是否统计使用浏览器 {localFileTime} {info}", + "localFileTime": "阅读本地文件的时间", + "localFilesInfo": "支持 PDF、图片、txt 以及 json 等格式", + "collectSiteName": "{input} 访问网站主页时,是否自动收集 {siteName} {siteNameUsage}", + "siteName": "网站的名称", + "siteNameUsage": "数据只存放在本地,将代替域名用于展示,增加辨识度。当然您可以自定义每个网站的名称" + }, + "dailyLimit": { + "filter": { + "label": "模态页面的背景风格 {input}", + "translucent": "半透明", + "groundGlass": "毛玻璃" + }, + "level": { + "label": "受限时如何解锁 {input}", + "nothing": "允许在管理页面直接解锁", + "password": "必须输入密码解锁", + "verification": "必须输入验证码解锁", + "passwordLabel": "解锁密码 {input}", + "verificationLabel": "验证码的难度 {input}", + "verificationDifficulty": { + "easy": "简单", + "hard": "困难", + "disgusting": "折磨" + } + } + }, + "backup": { + "title": "数据备份", + "type": "远端类型 {input}", + "client": "客户端标识 {input}", + "meta": { + "none": { + "label": "不开启备份" + }, + "gist": { + "authInfo": "需要创建一个至少包含 gist 权限的 token" + } + }, + "alert": "这是一项实验性功能,如果有任何问题请联系作者~ ({email})", + "test": "测试", + "lastTimeTip": "上次备份时间: {lastTime}", + "operation": "备份数据", + "auto": { + "label": "是否开启自动备份", + "interval": "每 {input} 分钟备份一次" + } + }, + "resetButton": "恢复默认", + "resetSuccess": "成功重置为默认值", + "defaultValue": "默认值: {default}" + }, + "zh_TW": { + "yes": "是", + "no": "否", + "popup": { + "title": "彈窗頁", + "max": "隻顯示前 {input} 條數據,剩下的條目合並顯示", + "defaultMergeDomain": "{input} 打開時合併子域名", + "defaultDisplay": "打開時顯示 {duration} {type}", + "displaySiteName": "{input} 顯示時是否使用 {siteName} 來代替域名", + "weekStart": "每週的第一天 {input}", + "weekStartAsNormal": "按照慣例" + }, + "appearance": { + "title": "外觀", + "displayWhitelist": "{input} 是否在 {contextMenu} 裡,顯示 {whitelist} 相關功能", + "whitelistItem": "白名單", + "contextMenu": "瀏覽器的右鍵菜單", + "displayBadgeText": "{input} 是否在 {icon} 上,顯示 {timeInfo}", + "icon": "擴充圖標", + "badgeTextContent": "當前網站的今日瀏覽時長", + "locale": { + "label": "語言設置 {input}", + "default": "跟隨瀏覽器", + "changeConfirm": "語言設置成功,請刷新頁麵!", + "reloadButton": "刷新" + }, + "printInConsole": { + "label": "{input} 是否在 {console} 裡打印當前網站的 {info}", + "console": "瀏覽器控制台", + "info": "今日拜訪信息" + }, + "darkMode": { + "label": "黑暗模式 {input}", + "options": { + "default": "跟隨瀏覽器", + "on": "始終開啟", + "off": "始終關閉", + "timed": "定時開啟" + } + } + }, + "statistics": { + "title": "統計", + "countWhenIdle": "{input} 是否統計 {idleTime} {info}", + "idleTime": "休眠時間", + "idleTimeInfo": "長時間不操作,比如全屏觀看視頻,瀏覽器會自動進入休眠狀態", + "countLocalFiles": "{input} 是否統計使用瀏覽器 {localFileTime} {info}", + "localFileTime": " 閱讀本地文件的時間 ", + "localFilesInfo": "支持 PDF、圖片、txt 以及 json 等格式", + "collectSiteName": "{input} 拜訪網站主頁時,是否自動收集 {siteName} {siteNameUsage}", + "siteName": "網站的名稱", + "siteNameUsage": "數據隻存放在本地,將代替域名用於展示,增加辨識度。當然您可以自定義每個網站的名稱" + }, + "dailyLimit": { + "filter": { + "label": "模態頁面的背景風格 {input}", + "translucent": "半透明", + "groundGlass": "毛玻璃" + }, + "title": "每日時限", + "level": { + "label": "受限時如何解鎖 {input}", + "nothing": "允許在管理頁面直接解鎖", + "password": "必須輸入密碼解鎖", + "verification": "必須輸入驗證碼解鎖", + "passwordLabel": "解鎖密碼 {input}", + "verificationLabel": "驗證碼的難度 {input}", + "verificationDifficulty": { + "easy": "簡單", + "hard": "困難", + "disgusting": "噁心" + } + } + }, + "backup": { + "title": "數據備份", + "type": "雲端類型 {input}", + "client": "客戶端標識 {input}", + "meta": { + "none": { + "label": "關閉備份" + }, + "gist": { + "authInfo": "需要創建一個至少包含 gist 權限的 token" + } + }, + "alert": "這是一項實驗性功能,如果有任何問題請聯繫作者 ({email}) ~", + "test": "測試", + "operation": "備份數據", + "lastTimeTip": "上次備份時間: {lastTime}", + "auto": { + "label": "是否開啟自動備份", + "interval": "每 {input} 分鐘備份一次" + } + }, + "resetButton": "恢複默認", + "resetSuccess": "成功重置爲默認值", + "defaultValue": "默認值: {default}" + }, + "en": { + "yes": "Yes", + "no": "No", + "popup": { + "title": "Popup Page", + "max": "Show the first {input} data items", + "defaultMergeDomain": "{input} Whether to merge subdomains on open", + "defaultDisplay": "Show {duration} {type} on open", + "displaySiteName": "{input} Whether to display {siteName} instead of URL", + "weekStart": "The first day for each week {input}", + "weekStartAsNormal": "As Normal" + }, + "appearance": { + "title": "Appearance", + "displayWhitelist": "{input} Whether to display {whitelist} in {contextMenu}", + "whitelistItem": "whitelist related shortcuts", + "contextMenu": "the context menu", + "displayBadgeText": "{input} Whether to display {timeInfo} in {icon}", + "icon": "the icon of extension", + "badgeTextContent": "the browsing time of current website", + "locale": { + "label": "Language {input}", + "default": "Follow browser", + "changeConfirm": "The language has been changed successfully, please reload this page!", + "reloadButton": "Reload" + }, + "printInConsole": { + "label": "{input} Whether to print {info} in the {console}", + "console": "console", + "info": "the visit count of the current website today" + }, + "darkMode": { + "label": "Dark mode {input}", + "options": { + "default": "Follow browser", + "on": "Always on", + "off": "Always off", + "timed": "Timed on" + } + } + }, + "statistics": { + "title": "Statistics", + "countWhenIdle": "{input} Whether to count {idleTime} {info}", + "idleTime": "idle time", + "idleTimeInfo": "If you do not operate for a long time (such as watching a video in full screen), the browser will automatically enter the idle state", + "countLocalFiles": "{input} Whether to count the time to {localFileTime} {info} in the browser", + "localFileTime": "read a local file", + "localFilesInfo": "Supports files of types such as PDF, image, txt and json", + "collectSiteName": "{input} Whether to automatically collect {siteName} {siteNameUsage} when visiting the site homepage", + "siteName": "the site name", + "siteNameUsage": "The data is only stored locally and will be displayed instead of the URL to increase the recognition.Of course, you can also customize the name of each site." + }, + "dailyLimit": { + "title": "Daily Limit", + "filter": { + "label": "The background style of the modal page {input}", + "translucent": "Translucent", + "groundGlass": "Ground Glass" + }, + "level": { + "label": "How to unlock while restricted {input}", + "nothing": "Allow direct unlocking on the admin page", + "password": "Must enter password to unlock", + "verification": "Must enter verification code to unlock", + "passwordLabel": "Password to unlock {input}", + "verificationLabel": "The difficulty of verification code {input}", + "verificationDifficulty": { + "easy": "Easy", + "hard": "Hard", + "disgusting": "Disgusting" + } + } + }, + "backup": { + "title": "Data Backup", + "type": "Remote type {input}", + "client": "Client name {input}", + "meta": { + "none": { + "label": "Always off" + }, + "gist": { + "authInfo": "One token with at least gist permission is required" + } + }, + "alert": "This is an experimental feature, if you have any questions please contact the author via {email}~", + "test": "Test", + "operation": "Backup", + "lastTimeTip": "Last backup time: {lastTime}", + "auto": { + "label": "Whether to enable automatic backup", + "interval": "and run every {input} minutes" + } + }, + "resetButton": "Reset", + "resetSuccess": "Reset to default successfully!", + "defaultValue": "Default: {default}" + }, + "ja": { + "yes": "はい", + "no": "いいえ", + "popup": { + "title": "ポップアップページ", + "max": "最初の {input} 個のデータのみを表示し、残りのエントリは結合されます", + "defaultMergeDomain": "{input} オープン時にサブドメインをマージ", + "defaultDisplay": "開くと {duration} {type} が表示されます", + "displaySiteName": "{input} ホストの代わりに {siteName} を表示するかどうか", + "weekStart": "週の最初の日 {input}", + "weekStartAsNormal": "いつものように" + }, + "appearance": { + "title": "外観", + "displayWhitelist": "{input} {contextMenu} に {whitelist} を表示するかどうか", + "whitelistItem": "ホワイトリスト機能", + "contextMenu": "コンテキストメニュー", + "displayBadgeText": "{input} {icon} に {timeInfo} を表示するかどうか", + "icon": "拡張機能のアイコン", + "badgeTextContent": "現在のウェブサイトの閲覧時間", + "locale": { + "label": "言語設定 {input}", + "default": "ブラウザと同じ", + "changeConfirm": "言語が正常に変更されました。このページをリロードしてください。", + "reloadButton": "リロード" + }, + "printInConsole": { + "label": "{input} 現在のウェブサイトの {info} を {console} に印刷するかどうか", + "console": "コンソール", + "info": "今日の情報をご覧ください" + }, + "darkMode": { + "label": "ダークモード {input}", + "options": { + "default": "ブラウザと同じ", + "on": "常にオン", + "off": "常にオフ", + "timed": "時限スタート" + } + } + }, + "statistics": { + "title": "統計", + "countWhenIdle": "{input} {idleTime} をカウントするかどうか {info}", + "idleTime": "アイドルタイム", + "idleTimeInfo": "長時間操作しない場合(フルスクリーンでビデオを見るなど)、ブラウザは自動的にアイドル状態になります", + "countLocalFiles": "{input} ブラウザで {localFileTime} {info} に費やされた時間をカウントするかどうか", + "localFileTime": "ローカルファイルの読み取り", + "localFilesInfo": "PDF、画像、txt、jsonを含む", + "collectSiteName": "{input} サイトのホームページにアクセスしたときに {siteName} {siteNameUsage} を自動的に収集するかどうか", + "siteName": "サイト名", + "siteNameUsage": "データはローカルにのみ存在し、認識を高めるためにホストの代わりに表示に使用されます。もちろん、各Webサイトの名前をカスタマイズできます。" + }, + "dailyLimit": { + "filter": { + "label": "モーダルページ {input} の背景スタイル", + "translucent": "半透明", + "groundGlass": "すりガラス" + }, + "title": "閲覧の制限", + "level": { + "label": "制限中のロック解除方法 {input}", + "nothing": "管理者ページでの直接のロック解除を許可する", + "password": "ロックを解除するにはパスワードを入力してください", + "verification": "ロックを解除するには認証コードを入力する必要があります", + "passwordLabel": "ロックを解除するためのパスワード {input}", + "verificationLabel": "認証コードの難しさ {input}", + "verificationDifficulty": { + "easy": "簡単", + "hard": "難しい", + "disgusting": "気持ち悪い" + } + } + }, + "backup": { + "title": "データバックアップ", + "type": "バックアップ方法 {input}", + "client": "クライアント名 {input}", + "meta": { + "none": { + "label": "バックアップを有効にしない" + }, + "gist": { + "authInfo": "少なくとも gist 権限を持つトークンが 1 つ必要です" + } + }, + "alert": "これは実験的な機能です。質問がある場合は、作成者に連絡してください ({email})", + "test": "テスト", + "operation": "バックアップ", + "lastTimeTip": "前回のバックアップ時間: {lastTime}", + "auto": { + "label": "自動バックアップを有効にするかどうか", + "interval": "{input} 分ごとに実行" + } + }, + "resetButton": "リセット", + "resetSuccess": "デフォルトに正常にリセット", + "defaultValue": "デフォルト値:{default}" + }, + "pt_PT": { + "yes": "Sim", + "no": "Não", + "resetButton": "Reiniciar", + "resetSuccess": "Redefinido para padrão com sucesso!", + "defaultValue": "Padrão: {default}", + "popup": { + "title": "Página de Pop-up", + "max": "Mostrar os primeiros {input} itens de dados", + "defaultMergeDomain": "{input} Se deseja mesclar subdomínios em aberto", + "defaultDisplay": "Mostrar {type} de {duration} em aberto", + "displaySiteName": "{input} Se deve exibir {siteName} em vez de URL", + "weekStart": "O primeiro dia de cada semana {input}", + "weekStartAsNormal": "Normal" + }, + "appearance": { + "title": "Aparência", + "displayWhitelist": "{input} Se deseja exibir {whitelist} em {contextMenu}", + "whitelistItem": "atalhos relacionados à lista ignorada", + "contextMenu": "o ementa de contexto", + "displayBadgeText": "{input} Se deve exibir {timeInfo} em {icon}", + "icon": "o ícone de extensão", + "badgeTextContent": "o tempo de navegação do site atual", + "locale": { + "label": "Idioma {input}", + "default": "Siga o navegador", + "changeConfirm": "O idioma foi alterado com sucesso, por favor, recarregue esta página!", + "reloadButton": "Recarregar" + }, + "printInConsole": { + "label": "{input} Se deseja imprimir {info} no {console}", + "console": "consola", + "info": "a contagem de visitas do site atual hoje" + }, + "darkMode": { + "label": "Modo escuro {input}", + "options": { + "default": "Siga o navegador", + "on": "Sempre ligado", + "off": "Sempre desativado", + "timed": "Cronometrando" + } + } + }, + "statistics": { + "title": "Estatísticas", + "countWhenIdle": "{input} Se quer contar {idleTime} {info}", + "idleTime": "tempo ocioso", + "idleTimeInfo": "Se você não operar por um longo período (como ver um vídeo em tela cheia), o navegador entrará automaticamente no estado ocioso", + "countLocalFiles": "{input} Se quer contar o tempo até {localFileTime} {info} no navegador", + "localFileTime": "ler um arquivo local", + "localFilesInfo": "Suporta arquivos de tipos como PDF, imagem, TXT e JSON", + "collectSiteName": "{input} Se deseja coletar automaticamente o {siteName} {siteNameUsage} quando visitar a página inicial do site", + "siteName": "o nome do site", + "siteNameUsage": "Os dados são armazenados apenas localmente e serão exibidos em vez da URL para aumentar o reconhecimento. F, também pode personalizar o nome de cada site." + }, + "dailyLimit": { + "filter": { + "label": "O estilo de fundo da página modal {input}", + "translucent": "Translúcido", + "groundGlass": "Vidro no Solo" + }, + "title": "Limite diário", + "level": { + "label": "Como desbloquear enquanto restrito {input}", + "nothing": "Permitir o desbloqueio direto na página de administração", + "password": "É necessário digitar a senha para desbloquear", + "verification": "É necessário digitar o código de verificação para desbloquear", + "passwordLabel": "Senha para desbloquear {input}", + "verificationLabel": "A dificuldade do código de verificação {input}", + "verificationDifficulty": { + "easy": "Fácil", + "hard": "Difícil", + "disgusting": "Nojento" + } + } + }, + "backup": { + "title": "Backup de Dados", + "type": "Tipo remoto {input}", + "client": "Nome do cliente {input}", + "meta": { + "none": { + "label": "Sempre desativado" + }, + "gist": { + "authInfo": "É necessário um token com pelo menos gist permissão" + } + }, + "test": "Testar", + "operation": "Backup", + "auto": { + "label": "Se deseja ativar o backup automático", + "interval": "e executar a cada {input} minutos" + }, + "lastTimeTip": "Hora do último backup: {lastTime}", + "alert": "Este é um recurso experimental, se tiver alguma dúvida, entre em contacto com o autor via {email}~" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/app/option.ts b/src/i18n/message/app/option.ts index dd34d377e..0751d80ea 100644 --- a/src/i18n/message/app/option.ts +++ b/src/i18n/message/app/option.ts @@ -1,10 +1,12 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import resource from './option-resource.json' + export type OptionMessage = { yes: string no: string @@ -14,7 +16,6 @@ export type OptionMessage = { defaultMergeDomain: string defaultDisplay: string displaySiteName: string - durationWidth: string weekStart: string weekStartAsNormal: string } @@ -43,9 +44,6 @@ export type OptionMessage = { label: string options: Record } - limitFilterType: Record & { - label: string - } } statistics: { title: string @@ -59,14 +57,30 @@ export type OptionMessage = { siteNameUsage: string siteName: string } + dailyLimit: { + filter: { + [filterType in timer.limit.FilterType]: string + } & { + label: string + } + level: { + [level in timer.limit.RestrictionLevel]: string + } & { + label: string + passwordLabel: string + verificationLabel: string + verificationDifficulty: { + [diff in timer.limit.VerificationDifficulty]: string + } + } + } backup: { title: string type: string client: string meta: { [type in timer.backup.Type]: { - label: string - auth?: string + label?: string authInfo?: string } } @@ -84,359 +98,6 @@ export type OptionMessage = { defaultValue: string } -const FOLLOW_BROWSER: Record = { - zh_CN: '跟随浏览器', - zh_TW: '跟隨瀏覽器', - en: 'Follow browser', - ja: 'ブラウザと同じ', -} - -const _default: Messages = { - zh_CN: { - yes: '是', - no: '否', - popup: { - title: '弹窗页', - max: '只显示前 {input} 条数据,剩下的条目合并显示', - defaultMergeDomain: '{input} 打开时合并子域名', - defaultDisplay: '打开时显示 {duration} {type}', - displaySiteName: '{input} 显示时是否使用 {siteName} 来代替域名', - durationWidth: '80px', - weekStart: '每周的第一天 {input}', - weekStartAsNormal: '按照惯例', - }, - appearance: { - title: '外观', - displayWhitelist: '{input} 是否在 {contextMenu} 里,显示 {whitelist} 相关功能', - whitelistItem: '白名单', - contextMenu: '浏览器的右键菜单', - displayBadgeText: '{input} 是否在 {icon} 上,显示 {timeInfo}', - icon: '扩展图标', - badgeTextContent: '当前网站的今日浏览时长', - locale: { - label: '语言设置 {input}', - default: '跟随浏览器', - changeConfirm: '语言设置成功,请刷新页面!', - reloadButton: '刷新', - }, - printInConsole: { - label: '{input} 是否在 {console} 里打印当前网站的 {info}', - console: '浏览器的控制台', - info: '今日访问信息', - }, - darkMode: { - label: '夜间模式 {input}', - options: { - default: '跟随浏览器', - on: '始终开启', - off: '始终关闭', - timed: '定时开启', - }, - }, - limitFilterType: { - label: '每日时限的背景风格 {input}', - translucent: '半透明', - groundGlass: '毛玻璃', - }, - }, - statistics: { - title: '统计', - countWhenIdle: '{input} 是否统计 {idleTime} {info}', - idleTime: '休眠时间', - idleTimeInfo: '长时间不操作(比如全屏观看视频),浏览器会自动进入休眠状态', - countLocalFiles: '{input} 是否统计使用浏览器 {localFileTime} {info}', - localFileTime: '阅读本地文件的时间', - localFilesInfo: '支持 PDF、图片、txt 以及 json 等格式', - collectSiteName: '{input} 访问网站主页时,是否自动收集 {siteName} {siteNameUsage}', - siteName: '网站的名称', - siteNameUsage: '数据只存放在本地,将代替域名用于展示,增加辨识度。当然您可以自定义每个网站的名称', - }, - backup: { - title: '数据备份', - type: '远端类型 {input}', - client: '客户端标识 {input}', - meta: { - none: { - label: '不开启备份', - auth: '', - }, - gist: { - label: 'Github Gist', - auth: 'Personal Access Token {info} {input}', - authInfo: '需要创建一个至少包含 gist 权限的 token', - }, - }, - alert: '这是一项实验性功能,如果有任何问题请联系作者~ (returnzhy1996@outlook.com)', - test: '测试', - lastTimeTip: '上次备份时间: {lastTime}', - operation: '备份数据', - auto: { - label: '是否开启自动备份', - interval: '每 {input} 分钟备份一次', - }, - }, - resetButton: '恢复默认', - resetSuccess: '成功重置为默认值', - defaultValue: '默认值: {default}', - }, - zh_TW: { - yes: '是', - no: '否', - popup: { - title: '彈窗頁', - max: '隻顯示前 {input} 條數據,剩下的條目合並顯示', - defaultMergeDomain: '{input} 打開時合併子域名', - defaultDisplay: '打開時顯示 {duration} {type}', - displaySiteName: '{input} 顯示時是否使用 {siteName} 來代替域名', - durationWidth: '80px', - weekStart: '每週的第一天 {input}', - weekStartAsNormal: '按照慣例', - }, - appearance: { - title: '外觀', - displayWhitelist: '{input} 是否在 {contextMenu} 裡,顯示 {whitelist} 相關功能', - whitelistItem: '白名單', - contextMenu: '瀏覽器的右鍵菜單', - displayBadgeText: '{input} 是否在 {icon} 上,顯示 {timeInfo}', - icon: '擴充圖標', - badgeTextContent: '當前網站的今日瀏覽時長', - locale: { - label: '語言設置 {input}', - default: '跟隨瀏覽器', - changeConfirm: '語言設置成功,請刷新頁麵!', - reloadButton: '刷新', - }, - printInConsole: { - label: '{input} 是否在 {console} 裡打印當前網站的 {info}', - console: '瀏覽器控制台', - info: '今日拜訪信息', - }, - darkMode: { - label: '黑暗模式 {input}', - options: { - default: '跟隨瀏覽器', - on: '始終開啟', - off: '始終關閉', - timed: '定時開啟', - }, - }, - limitFilterType: { - label: '每日時限的背景風格 {input}', - translucent: '半透明', - groundGlass: '毛玻璃', - }, - }, - statistics: { - title: '統計', - countWhenIdle: '{input} 是否統計 {idleTime} {info}', - idleTime: '休眠時間', - idleTimeInfo: '長時間不操作(比如全屏觀看視頻),瀏覽器會自動進入休眠狀態', - countLocalFiles: '{input} 是否統計使用瀏覽器 {localFileTime} {info}', - localFileTime: '閱讀本地文件的時間', - localFilesInfo: '支持 PDF、圖片、txt 以及 json 等格式', - collectSiteName: '{input} 拜訪網站主頁時,是否自動收集 {siteName} {siteNameUsage}', - siteName: '網站的名稱', - siteNameUsage: '數據隻存放在本地,將代替域名用於展示,增加辨識度。當然您可以自定義每個網站的名稱', - }, - backup: { - title: '數據備份', - type: '雲端類型 {input}', - client: '客戶端標識 {input}', - meta: { - none: { - label: '關閉備份', - }, - gist: { - label: 'Github Gist', - auth: 'Personal Access Token {info} {input}', - authInfo: '需要創建一個至少包含 gist 權限的 token', - }, - }, - alert: '這是一項實驗性功能,如果有任何問題請聯繫作者 (returnzhy1996@outlook.com) ~', - test: '測試', - operation: '備份數據', - lastTimeTip: '上次備份時間: {lastTime}', - auto: { - label: '是否開啟自動備份', - interval: '每 {input} 分鐘備份一次', - }, - }, - resetButton: '恢複默認', - resetSuccess: '成功重置爲默認值', - defaultValue: '默認值: {default}', - }, - en: { - yes: 'Yes', - no: 'No', - popup: { - title: 'Popup Page', - max: 'Show the first {input} data items', - defaultMergeDomain: '{input} Whether to merge subdomains on open', - defaultDisplay: 'Show {duration} {type} on open', - displaySiteName: '{input} Whether to display {siteName} instead of URL', - durationWidth: '110px', - weekStart: 'The first day for each week {input}', - weekStartAsNormal: 'As Normal', - }, - appearance: { - title: 'Appearance', - displayWhitelist: '{input} Whether to display {whitelist} in {contextMenu}', - whitelistItem: 'whitelist related shortcuts', - contextMenu: 'the context menu', - displayBadgeText: '{input} Whether to display {timeInfo} in {icon}', - icon: 'the icon of extension', - badgeTextContent: 'the browsing time of current website', - locale: { - label: 'Language {input}', - default: 'Follow browser', - changeConfirm: 'The language has been changed successfully, please reload this page!', - reloadButton: 'Reload', - }, - printInConsole: { - label: '{input} Whether to print {info} in the {console}', - console: 'console', - info: 'the visit count of the current website today', - }, - darkMode: { - label: 'Dark mode {input}', - options: { - default: 'Follow browser', - on: 'Always on', - off: 'Always off', - timed: 'Timed on', - }, - }, - limitFilterType: { - label: 'Background style for daily time limit {input}', - translucent: 'Translucent', - groundGlass: 'Ground Glass', - }, - }, - statistics: { - title: 'Statistics', - countWhenIdle: '{input} Whether to count {idleTime} {info}', - idleTime: 'idle time', - idleTimeInfo: 'If you do not operate for a long time (such as watching a video in full screen), the browser will automatically enter the idle state', - countLocalFiles: '{input} Whether to count the time to {localFileTime} {info} in the browser', - localFileTime: ' read a local file ', - localFilesInfo: 'Supports files of types such as PDF, image, txt and json', - collectSiteName: '{input} Whether to automatically collect {siteName} {siteNameUsage} when visiting the site homepage', - siteName: ' the site name ', - siteNameUsage: 'The data is only stored locally and will be displayed instead of the URL to increase the recognition.Of course, you can also customize the name of each site.', - }, - backup: { - title: 'Data Backup', - type: 'Remote type {input}', - client: 'Client name {input}', - meta: { - none: { - label: 'Always off', - }, - gist: { - label: 'Github Gist', - auth: 'Personal Access Token {info} {input}', - authInfo: 'One token with at least gist permission is required', - }, - }, - alert: 'This is an experimental feature, if you have any questions please contact the author via returnzhy1996@outlook.com~', - test: 'Test', - operation: 'Backup', - lastTimeTip: 'Last backup time: {lastTime}', - auto: { - label: 'Whether to enable automatic backup', - interval: 'and run every {input} minutes', - }, - }, - resetButton: 'Reset', - resetSuccess: 'Reset to default successfully!', - defaultValue: 'Default: {default}', - }, - ja: { - yes: 'はい', - no: 'いいえ', - popup: { - title: 'ポップアップページ', - max: '最初の {input} 個のデータのみを表示し、残りのエントリは結合されます', - defaultMergeDomain: '{input} オープン時にサブドメインをマージ', - defaultDisplay: '開くと {duration} {type} が表示されます', - displaySiteName: '{input} ホストの代わりに {siteName} を表示するかどうか', - durationWidth: '100px', - weekStart: '週の最初の日 {input}', - weekStartAsNormal: 'いつものように', - }, - appearance: { - title: '外観', - displayWhitelist: '{input} {contextMenu} に {whitelist} を表示するかどうか', - whitelistItem: 'ホワイトリスト機能', - contextMenu: 'コンテキストメニュー', - displayBadgeText: '{input} {icon} に {timeInfo} を表示するかどうか', - icon: '拡張機能のアイコン', - badgeTextContent: '現在のウェブサイトの閲覧時間', - locale: { - label: '言語設定 {input}', - default: 'ブラウザと同じ', - changeConfirm: '言語が正常に変更されました。このページをリロードしてください。', - reloadButton: 'リロード', - }, - printInConsole: { - label: '{input} 現在のウェブサイトの {info} を {console} に印刷するかどうか', - console: 'コンソール', - info: '今日の情報をご覧ください', - }, - darkMode: { - label: 'ダークモード {input}', - options: { - default: 'ブラウザと同じ', - on: '常にオン', - off: '常にオフ', - timed: '時限スタート', - }, - }, - limitFilterType: { - label: '毎日の時間制限の背景スタイル {input}', - translucent: '半透明', - groundGlass: 'すりガラス', - }, - }, - statistics: { - title: '統計', - countWhenIdle: '{input} {idleTime} をカウントするかどうか {info}', - idleTime: 'アイドルタイム', - idleTimeInfo: '長時間操作しない場合(フルスクリーンでビデオを見るなど)、ブラウザは自動的にアイドル状態になります', - countLocalFiles: '{input} ブラウザで {localFileTime} {info} に費やされた時間をカウントするかどうか', - localFileTime: ' ローカルファイルの読み取り ', - localFilesInfo: 'PDF、画像、txt、jsonを含む', - collectSiteName: '{input} ウェブサイトのホームページにアクセスしたときにウェブサイトの名前を自動的に収集するかどうか', - siteName: 'サイト名', - siteNameUsage: 'データはローカルにのみ存在し、認識を高めるためにホストの代わりに表示に使用されます。もちろん、各Webサイトの名前をカスタマイズできます。', - }, - backup: { - title: 'データバックアップ', - type: 'バックアップ方法 {input}', - client: 'クライアント名 {input}', - meta: { - none: { - label: 'バックアップを有効にしない', - }, - gist: { - label: 'Github Gist', - auth: 'Personal Access Token {info} {input}', - authInfo: '少なくとも gist 権限を持つトークンが 1 つ必要です', - }, - }, - alert: 'これは実験的な機能です。質問がある場合は、作成者に連絡してください (returnzhy1996@outlook.com)', - test: 'テスト', - operation: 'バックアップ', - lastTimeTip: '前回のバックアップ時間: {lastTime}', - auto: { - label: '自動バックアップを有効にするかどうか', - interval: ' {input} 分ごとに実行', - }, - }, - resetButton: 'リセット', - resetSuccess: 'デフォルトに正常にリセット', - defaultValue: 'デフォルト値:{default}', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/report-resource.json b/src/i18n/message/app/report-resource.json new file mode 100644 index 000000000..e50886d1e --- /dev/null +++ b/src/i18n/message/app/report-resource.json @@ -0,0 +1,132 @@ +{ + "zh_CN": { + "mergeDate": "合并日期", + "mergeDomain": "合并子域名", + "hostPlaceholder": "请输入域名,然后回车", + "exportFileName": "我的上网时间", + "added2Whitelist": "成功加入白名单", + "removeFromWhitelist": "成功从白名单移除", + "batchDelete": { + "buttonText": "批量删除", + "noSelectedMsg": "请先在表格中勾选需要删除的行", + "confirmMsg": "{example} 等网站在 {date} 的 {count} 条记录将会被删除!", + "confirmMsgAll": "{example} 等网站的 {count} 条记录将会被删除!", + "confirmMsgRange": "{example} 等网站在 {start} 至 {end} 之间的 {count} 条记录将会被删除!", + "successMsg": "成功批量删除" + }, + "remoteReading": { + "on": "正在查询远端备份数据", + "off": "单击以开启远端备份数据查询功能", + "table": { + "client": "客户端", + "localData": "本地数据", + "value": "对应数值", + "percentage": "百分比" + } + } + }, + "zh_TW": { + "mergeDate": "合並日期", + "mergeDomain": "合並子域名", + "hostPlaceholder": "請輸入域名,然後回車", + "exportFileName": "我的上網時間", + "added2Whitelist": "成功加入白名單", + "removeFromWhitelist": "成功從白名單移除", + "batchDelete": { + "buttonText": "批量刪除", + "noSelectedMsg": "請先在表格中勾選需要刪除的行", + "confirmMsg": "{example} 等網站在 {date} 的 {count} 條記錄將會被刪除!", + "confirmMsgAll": "{example} 等網站的 {count} 條記錄將會被刪除!", + "confirmMsgRange": "{example} 等網站在 {start} 至 {end} 之間的 {count} 條記錄將會被刪除!", + "successMsg": "成功批量刪除" + }, + "remoteReading": { + "on": "正在查詢遠端備份數據", + "off": "單擊以開啟遠端備份數據查詢功能", + "table": { + "client": "客户端", + "localData": "本地數據", + "value": "對應數值", + "percentage": "百分比" + } + } + }, + "en": { + "mergeDate": "Merge date", + "mergeDomain": "Merge URL", + "hostPlaceholder": "Partial URL, then enter", + "exportFileName": "My_Browsing_Time", + "added2Whitelist": "Added into the whitelist", + "removeFromWhitelist": "Removed from the whitelist", + "batchDelete": { + "buttonText": "Batch delete", + "noSelectedMsg": "Please select the row you want to delete in the table first", + "confirmMsg": "{count} records for sites like {example} on {date} will be deleted!", + "confirmMsgAll": "{count} records for sites like {example} will be deleted!", + "confirmMsgRange": "{count} records for sites like {example} between {start} and {end} will be deleted!", + "successMsg": "Batch delete successfully" + }, + "remoteReading": { + "on": "Reading remote backuped data", + "off": "Click to read remote backuped data", + "table": { + "client": "Client's Name", + "localData": "Local Data", + "value": "Value", + "percentage": "Percentage" + } + } + }, + "ja": { + "mergeDate": "マージ日", + "mergeDomain": "URLをマージ", + "hostPlaceholder": "URL を入力してください", + "exportFileName": "私のウェブ時間データ", + "added2Whitelist": "ホワイトリストに正常に追加されました", + "removeFromWhitelist": "ホワイトリストから正常に削除されました", + "batchDelete": { + "buttonText": "バッチ削除", + "noSelectedMsg": "最初にテーブルで削除する行にチェックマークを付けてください", + "confirmMsg": "{date} の {example} のようなサイトの {count} レコードは削除されます!", + "confirmMsgAll": "{example} のようなサイトの {count} レコードは削除されます!", + "confirmMsgRange": "{start} と {end} の間の {example} のようなサイトの {count} レコードが削除されます!", + "successMsg": "バッチ削除に成功" + }, + "remoteReading": { + "on": "リモート バックアップ データのクエリ", + "off": "クリックして、リモート バックアップ データのクエリ機能を有効にします", + "table": { + "client": "クライアントの名前", + "localData": "ローカル データ", + "value": "対応する値", + "percentage": "パーセンテージ" + } + } + }, + "pt_PT": { + "mergeDate": "Mesclar Datas", + "mergeDomain": "Mesclar URL", + "hostPlaceholder": "URL parcial, e depois digite", + "exportFileName": "Meu_Tempo_de_Navegação", + "added2Whitelist": "Adicionada à lista ignorada", + "removeFromWhitelist": "Removido da lista ignorada", + "batchDelete": { + "buttonText": "Apagar em lote", + "noSelectedMsg": "Por favor, selecione a linha que deseja excluir na tabela primeiro", + "confirmMsg": "{count} registros de sites como {example} em {date} serão excluídos!", + "confirmMsgAll": "{count} registros para sites como {example} serão excluídos!", + "confirmMsgRange": "{count} registros para sites como {example} entre {start} e {end} serão excluídos!", + "successMsg": "Lote excluído com sucesso" + }, + "remoteReading": { + "on": "Lendo dados de backup remoto", + "off": "Clique para ler os dados de backup remoto", + "table": { + "client": "Nome do Cliente", + "localData": "Dados Locais", + "value": "Valor", + "percentage": "Percentagem" + } + } + } +} \ No newline at end of file diff --git a/src/i18n/message/app/report.ts b/src/i18n/message/app/report.ts index cb8d79cfa..a8d05255e 100644 --- a/src/i18n/message/app/report.ts +++ b/src/i18n/message/app/report.ts @@ -5,13 +5,9 @@ * https://opensource.org/licenses/MIT */ +import resource from './report-resource.json' + export type ReportMessage = { - startDate: string - endDate: string - lastWeek: string - last30Days: string - today: string - yesterday: string mergeDate: string mergeDomain: string hostPlaceholder: string @@ -38,135 +34,6 @@ export type ReportMessage = { } } -const _default: Messages = { - zh_CN: { - startDate: '开始日期', - endDate: '结束日期', - lastWeek: '最近一周', - last30Days: '最近 30 天', - today: '今天', - yesterday: '昨天', - mergeDate: '合并日期', - mergeDomain: '合并子域名', - hostPlaceholder: '请输入域名,然后回车', - exportFileName: '我的上网时间', - added2Whitelist: '成功加入白名单', - removeFromWhitelist: '成功从白名单移除', - batchDelete: { - buttonText: '批量删除', - noSelectedMsg: '请先在表格中勾选需要删除的行', - confirmMsg: '{example} 等网站在 {date} 的 {count} 条记录将会被删除!', - confirmMsgAll: '{example} 等网站的 {count} 条记录将会被删除!', - confirmMsgRange: '{example} 等网站在 {start} 至 {end} 之间的 {count} 条记录将会被删除!', - successMsg: '成功批量删除', - }, - remoteReading: { - on: '正在查询远端备份数据', - off: '单击以开启远端备份数据查询功能', - table: { - client: '客户端', - localData: '本地数据', - value: '对应数值', - percentage: '百分比', - }, - }, - }, - zh_TW: { - startDate: '開始日期', - endDate: '結束日期', - lastWeek: '最近一週', - last30Days: '最近 30 天', - today: '今天', - yesterday: '昨天', - mergeDate: '合並日期', - mergeDomain: '合並子域名', - hostPlaceholder: '請輸入域名,然後回車', - exportFileName: '我的上網時間', - added2Whitelist: '成功加入白名單', - removeFromWhitelist: '成功從白名單移除', - batchDelete: { - buttonText: '批量刪除', - noSelectedMsg: '請先在表格中勾選需要刪除的行', - confirmMsg: '{example} 等網站在 {date} 的 {count} 條記錄將會被刪除!', - confirmMsgAll: '{example} 等網站的 {count} 條記錄將會被刪除!', - confirmMsgRange: '{example} 等網站在 {start} 至 {end} 之間的 {count} 條記錄將會被刪除!', - successMsg: '成功批量刪除', - }, - remoteReading: { - on: '正在查詢遠端備份數據', - off: '單擊以開啟遠端備份數據查詢功能', - table: { - client: '客户端', - localData: '本地數據', - value: '對應數值', - percentage: '百分比', - }, - }, - }, - en: { - startDate: 'Start date', - endDate: 'End date', - lastWeek: 'Last week', - last30Days: 'Last 30 days', - today: 'Today', - yesterday: 'Yesterday', - mergeDate: 'Merge date', - mergeDomain: 'Merge URL', - hostPlaceholder: 'Partial URL, then enter', - exportFileName: 'Timer_Data', - added2Whitelist: 'Added into the whitelist', - removeFromWhitelist: 'Removed from the whitelist', - batchDelete: { - buttonText: 'Batch delete', - noSelectedMsg: 'Please select the row you want to delete in the table first', - confirmMsg: '{count} records for sites like {example} on {date} will be deleted!', - confirmMsgAll: '{count} records for sites like {example} will be deleted!', - confirmMsgRange: '{count} records for sites like {example} between {start} and {end} will be deleted!', - successMsg: 'Batch delete successfully', - }, - remoteReading: { - on: 'Reading remote backuped data', - off: 'Click to read remote backuped data', - table: { - client: 'Client\'s Name', - localData: 'Local Data', - value: 'Value', - percentage: 'Percentage', - }, - }, - }, - ja: { - startDate: '開始日', - endDate: '終了日', - lastWeek: '先週', - last30Days: '過去 30 日間', - today: '今日', - yesterday: '昨日', - mergeDate: 'マージ日', - mergeDomain: 'URLをマージ', - hostPlaceholder: 'URL を入力してください', - exportFileName: '私のウェブ時間データ', - added2Whitelist: 'ホワイトリストに正常に追加されました', - removeFromWhitelist: 'ホワイトリストから正常に削除されました', - batchDelete: { - buttonText: 'バッチ削除', - noSelectedMsg: '最初にテーブルで削除する行にチェックマークを付けてください', - confirmMsg: '{date} の {example} のようなサイトの {count} レコードは削除されます!', - confirmMsgAll: '{example} のようなサイトの {count} レコードは削除されます!', - confirmMsgRange: '{start} と {end} の間の {example} のようなサイトの {count} レコードが削除されます!', - successMsg: 'バッチ削除に成功', - }, - remoteReading: { - on: 'リモート バックアップ データのクエリ', - off: 'クリックして、リモート バックアップ データのクエリ機能を有効にします', - table: { - client: 'クライアントの名前', - localData: 'ローカル データ', - value: '対応する値', - percentage: 'パーセンテージ', - }, - }, - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/site-manage-resource.json b/src/i18n/message/app/site-manage-resource.json new file mode 100644 index 000000000..d1c40b5a0 --- /dev/null +++ b/src/i18n/message/app/site-manage-resource.json @@ -0,0 +1,242 @@ +{ + "zh_CN": { + "hostPlaceholder": "请输入域名,然后回车", + "aliasPlaceholder": "请输入网站名,然后回车", + "onlyDetected": "只看自动抓取", + "deleteConfirmMsg": "{host} 的名称设置将会被删除", + "column": { + "host": "网站域名", + "type": "网站类型", + "alias": "网站名称", + "aliasInfo": "网站名称会在报表以及今日数据(需要在扩展选项里设置)里展示,方便您快速识别域名", + "source": "名称来源", + "icon": "网站图标" + }, + "type": { + "normal": { + "name": "普通站点", + "info": "按域名的维度统计" + }, + "merged": { + "name": "合并站点", + "info": "将多个相关的域名合并统计,合并规则可以自定义" + }, + "virtual": { + "name": "自定义站点", + "info": "统计 Ant Pattern 格式的任意 URL,可以在右上角新增自定义站点" + } + }, + "source": { + "user": "手动设置", + "detected": "自动抓取" + }, + "button": { + "add": "新增", + "delete": "删除", + "save": "保存" + }, + "form": { + "emptyAlias": "请输入网站名称", + "emptyHost": "请输入网站域名" + }, + "msg": { + "hostExistWarn": "{host} 已经存在", + "saved": "已保存", + "existedTag": "已存在", + "mergedTag": "合并", + "virtualTag": "自定义" + } + }, + "zh_TW": { + "hostPlaceholder": "請輸入網域,然後回車", + "aliasPlaceholder": "請輸入網站名,然後回車", + "onlyDetected": "隻看自動抓取", + "deleteConfirmMsg": "{host} 的名稱設置將會被刪除", + "column": { + "host": "網站域名", + "type": "網站類型", + "alias": "網站名稱", + "aliasInfo": "網站名稱會在報表以及今日數據(需要在擴充選項裡設置)裡展示,方便您快速識別網域", + "source": "名稱來源", + "icon": "網站圖標" + }, + "source": { + "user": "手動設置", + "detected": "自動抓取" + }, + "type": { + "normal": { + "name": "普通站點", + "info": "按域名的維度統計" + }, + "merged": { + "name": "合併站點", + "info": "將多個相關的域名合併統計,合併規則可以自定義" + }, + "virtual": { + "name": "自定義站點", + "info": "統計 Ant Pattern 格式的任意 URL,可以在右上角新增自定義站點" + } + }, + "button": { + "add": "新增", + "delete": "刪除", + "save": "保存" + }, + "form": { + "emptyAlias": "請輸入網站名稱", + "emptyHost": "請輸入網站域名" + }, + "msg": { + "hostExistWarn": "{host} 已經存在", + "saved": "已保存", + "existedTag": "已存在", + "mergedTag": "合並", + "virtualTag": "虛擬" + } + }, + "en": { + "hostPlaceholder": "Partial URL, then enter", + "aliasPlaceholder": "Partial name, then enter", + "onlyDetected": "Only detected", + "deleteConfirmMsg": "The name of {host} will be deleted", + "column": { + "host": "Site URL", + "type": "Site Type", + "alias": "Site Name", + "aliasInfo": "The site name will be shown on the record page and the popup page", + "source": "Name Source", + "icon": "Icon" + }, + "type": { + "normal": { + "name": "normal", + "info": "statistics by domain name" + }, + "merged": { + "name": "merged", + "info": "merge statistics of multiple related domain names, and the merge rules can be customized" + }, + "virtual": { + "name": "virtual", + "info": "count any URL in Ant Pattern format, you can add a custom site in the upper right corner" + } + }, + "source": { + "user": "user-maintained", + "detected": "auto-detected" + }, + "button": { + "add": "New", + "delete": "Delete", + "save": "Save" + }, + "form": { + "emptyAlias": "Please enter site name", + "emptyHost": "Please enter site URL" + }, + "msg": { + "hostExistWarn": "{host} exists", + "saved": "Saved", + "existedTag": "EXISTED", + "mergedTag": "MERGED", + "virtualTag": "VIRTUAL" + } + }, + "ja": { + "hostPlaceholder": "ドメイン名で検索", + "aliasPlaceholder": "サイト名で検索", + "onlyDetected": "検出されただけ", + "deleteConfirmMsg": "{host} の名前が削除されます", + "column": { + "host": "サイトのURL", + "alias": "サイト名", + "aliasInfo": "サイト名はレコードページとポップアップページに表示されます", + "source": "ソース", + "type": "サイト種別", + "icon": "Icon" + }, + "source": { + "user": "手动输入", + "detected": "システム検出" + }, + "button": { + "add": "追加", + "delete": "削除", + "save": "保存" + }, + "form": { + "emptyAlias": "サイト名を入力してください", + "emptyHost": "ドメイン名を入力してください" + }, + "msg": { + "hostExistWarn": "{host} が存在します", + "saved": "保存しました", + "existedTag": "既存", + "mergedTag": "合并", + "virtualTag": "バーチャル" + }, + "type": { + "normal": { + "name": "普通", + "info": "ドメイン名による統計" + }, + "merged": { + "name": "合并", + "info": "関連する複数のドメイン名の統計をマージし、マージ ルールをカスタマイズできます。" + }, + "virtual": { + "name": "バーチャル", + "info": "Ant Pattern 形式の任意の URL をカウントします。右上隅にカスタムサイトを追加できます" + } + } + }, + "pt_PT": { + "hostPlaceholder": "URL parcial, e depois digite", + "aliasPlaceholder": "Nome parcial, e depois digite", + "onlyDetected": "Apenas detetado", + "deleteConfirmMsg": "O nome do {host} será apagado", + "column": { + "host": "URL do Site", + "alias": "Nome do Site", + "aliasInfo": "O nome do site será mostrado na página de registro e na página pop-up", + "source": "Nome Fonte", + "type": "Categoria de Sítio Web", + "icon": "Ícone" + }, + "source": { + "user": "usuário mantido", + "detected": "detetado" + }, + "button": { + "add": "Criar", + "delete": "Apagar", + "save": "Salvar" + }, + "form": { + "emptyAlias": "Por favor, insira um nome de site", + "emptyHost": "Por favor, insira um URL de site" + }, + "msg": { + "hostExistWarn": "{host} existe", + "saved": "Guardado", + "existedTag": "EXISTADO", + "mergedTag": "MERGADO", + "virtualTag": "VIRTUAL" + }, + "type": { + "normal": { + "name": "normal", + "info": "estatísticas por nome de domínio" + }, + "merged": { + "name": "mesclado", + "info": "estatísticas de mesclagem de vários domínios relacionados, e as regras de mesclagem podem ser personalizadas" + }, + "virtual": { + "name": "virtual", + "info": "conta qualquer URL no formato Ant Pattern, pode adicionar um site personalizado no canto superior direito" + } + } + } +} \ No newline at end of file diff --git a/src/i18n/message/app/site-manage.ts b/src/i18n/message/app/site-manage.ts index 51488b5ab..dc39f02a7 100644 --- a/src/i18n/message/app/site-manage.ts +++ b/src/i18n/message/app/site-manage.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './site-manage-resource.json' + export type SiteManageMessage = { hostPlaceholder: string aliasPlaceholder: string @@ -41,198 +43,6 @@ export type SiteManageMessage = { } } -const _default: Messages = { - zh_CN: { - hostPlaceholder: '请输入域名,然后回车', - aliasPlaceholder: '请输入网站名,然后回车', - onlyDetected: '只看自动抓取', - deleteConfirmMsg: '{host} 的名称设置将会被删除', - column: { - host: '网站域名', - type: '网站类型', - alias: '网站名称', - aliasInfo: '网站名称会在报表以及今日数据(需要在扩展选项里设置)里展示,方便您快速识别域名', - source: '名称来源', - icon: '网站图标', - }, - type: { - normal: { - name: '普通站点', - info: '按域名的维度统计', - }, - merged: { - name: '合并站点', - info: '将多个相关的域名合并统计,合并规则可以自定义', - }, - virtual: { - name: '自定义站点', - info: '统计 Ant Pattern 格式的任意 URL,可以在右上角新增自定义站点', - }, - }, - source: { - user: '手动设置', - detected: '自动抓取', - }, - button: { - add: '新增', - delete: '删除', - save: '保存', - }, - form: { - emptyAlias: '请输入网站名称', - emptyHost: '请输入网站域名', - }, - msg: { - hostExistWarn: '{host} 已经存在', - saved: '已保存', - existedTag: '已存在', - mergedTag: '合并', - virtualTag: '自定义', - }, - }, - zh_TW: { - hostPlaceholder: '請輸入網域,然後回車', - aliasPlaceholder: '請輸入網站名,然後回車', - onlyDetected: '隻看自動抓取', - deleteConfirmMsg: '{host} 的名稱設置將會被刪除', - column: { - host: '網站域名', - type: '網站類型', - alias: '網站名稱', - aliasInfo: '網站名稱會在報表以及今日數據(需要在擴充選項裡設置)裡展示,方便您快速識別網域', - source: '名稱來源', - icon: '網站圖標', - }, - source: { - user: '手動設置', - detected: '自動抓取', - }, - type: { - normal: { - name: '普通站點', - info: '按域名的維度統計', - }, - merged: { - name: '合併站點', - info: '將多個相關的域名合併統計,合併規則可以自定義', - }, - virtual: { - name: '自定義站點', - info: '統計 Ant Pattern 格式的任意 URL,可以在右上角新增自定義站點', - }, - }, - button: { - add: '新增', - delete: '刪除', - save: '保存', - }, - form: { - emptyAlias: '請輸入網站名稱', - emptyHost: '請輸入網站域名', - }, - msg: { - hostExistWarn: '{host} 已經存在', - saved: '已保存', - existedTag: '已存在', - mergedTag: '合並', - }, - }, - en: { - hostPlaceholder: 'Partial URL, then enter', - aliasPlaceholder: 'Partial name, then enter', - onlyDetected: 'Only detected', - deleteConfirmMsg: 'The name of {host} will be deleted', - column: { - host: 'Site URL', - type: 'Site Type', - alias: 'Site Name', - aliasInfo: 'The site name will be shown on the record page and the popup page', - source: 'Name Source', - icon: 'Icon', - }, - type: { - normal: { - name: 'normal', - info: 'statistics by domain name', - }, - merged: { - name: 'merged', - info: 'merge statistics of multiple related domain names, and the merge rules can be customized', - }, - virtual: { - name: 'virtual', - info: 'count any URL in Ant Pattern format, you can add a custom site in the upper right corner', - }, - }, - source: { - user: 'user-maintained', - detected: 'auto-detected', - }, - button: { - add: 'New', - delete: 'Delete', - save: 'Save', - }, - form: { - emptyAlias: 'Please enter site name', - emptyHost: 'Please enter site URL', - }, - msg: { - hostExistWarn: '{host} exists', - saved: 'Saved', - existedTag: 'EXISTED', - mergedTag: 'MERGED', - virtualTag: 'VIRTUAL', - }, - }, - ja: { - hostPlaceholder: 'ドメイン名で検索', - aliasPlaceholder: 'サイト名で検索', - onlyDetected: '検出されただけ', - deleteConfirmMsg: '{host} の名前が削除されます', - column: { - host: 'サイトのURL', - alias: 'サイト名', - aliasInfo: 'サイト名はレコードページとポップアップページに表示されます', - source: 'ソース', - type: 'サイト種別', - icon: 'Icon', - }, - source: { - user: '手动输入', - detected: 'システム検出', - }, - button: { - add: '追加', - delete: '削除', - save: '保存', - }, - form: { - emptyAlias: 'サイト名を入力してください', - emptyHost: 'ドメイン名を入力してください', - }, - msg: { - hostExistWarn: '{host} が存在します', - saved: '保存しました', - existedTag: '既存', - mergedTag: '合并', - virtualTag: 'バーチャル', - }, - type: { - normal: { - name: '普通', - info: 'ドメイン名による統計', - }, - merged: { - name: '合并', - info: '複数の関連するドメイン名のマージ統計をカスタマイズできます', - }, - virtual: { - name: 'バーチャル', - info: 'Ant Pattern 形式の任意の URL をカウントします。右上隅にカスタムサイトを追加できます', - }, - }, - }, -} +const _default: Messages = resource export default _default diff --git a/src/i18n/message/app/time-format-resource.json b/src/i18n/message/app/time-format-resource.json new file mode 100644 index 000000000..066590f8d --- /dev/null +++ b/src/i18n/message/app/time-format-resource.json @@ -0,0 +1,32 @@ +{ + "zh_CN": { + "default": "默认时间格式", + "hour": "按小时显示", + "minute": "按分钟显示", + "second": "按秒显示" + }, + "zh_TW": { + "default": "默認時間格式", + "hour": "按小時顯示", + "minute": "按分鐘顯示", + "second": "按秒顯示" + }, + "en": { + "default": "Default time format", + "hour": "Display in hours", + "minute": "Display in minutes", + "second": "Display in seconds" + }, + "ja": { + "default": "デフォルトの時間形式", + "hour": "時間単位で表示", + "minute": "分単位で表示", + "second": "秒単位で表示" + }, + "pt_PT": { + "default": "Formato padrão da hora", + "hour": "Exibir em horas", + "minute": "Exibir em minutos", + "second": "Exibir em segundos" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/time-format.ts b/src/i18n/message/app/time-format.ts index 25055d4e9..9a8c456e7 100644 --- a/src/i18n/message/app/time-format.ts +++ b/src/i18n/message/app/time-format.ts @@ -5,33 +5,10 @@ * https://opensource.org/licenses/MIT */ +import resource from './time-format-resource.json' + export type TimeFormatMessage = { [key in timer.app.TimeFormat]: string } -const _default: Messages = { - zh_CN: { - default: '默认时间格式', - hour: '按小时显示', - minute: '按分钟显示', - second: '按秒显示', - }, - zh_TW: { - default: '默認時間格式', - hour: '按小時顯示', - minute: '按分鐘顯示', - second: '按秒顯示', - }, - en: { - default: 'Default time format', - hour: 'Display in hours', - minute: 'Display in minutes', - second: 'Display in seconds', - }, - ja: { - default: 'デフォルトの時間形式', - hour: '時間単位で表示', - minute: '分単位で表示', - second: '秒単位で表示', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/whitelist-resource.json b/src/i18n/message/app/whitelist-resource.json new file mode 100644 index 000000000..e43f3d655 --- /dev/null +++ b/src/i18n/message/app/whitelist-resource.json @@ -0,0 +1,52 @@ +{ + "zh_CN": { + "addConfirmMsg": "{url} 加入白名单后,将不再统计该网站的数据", + "removeConfirmMsg": "{url} 将从白名单中移除", + "duplicateMsg": "已存在白名单中", + "infoAlertTitle": "你可以在这里配置网站白名单", + "infoAlert0": "白名单内网站的上网时长和打开次数不会被统计", + "infoAlert1": "白名单内网站的上网时间也不会被限制", + "placeholder": "域名", + "errorInput": "域名格式错误" + }, + "zh_TW": { + "addConfirmMsg": "{url} 加入白名單後,將不再統計該網站的數據", + "removeConfirmMsg": "{url} 將從白名單中移除", + "duplicateMsg": "已存在白名單中", + "infoAlertTitle": "你可以在這裡配置網站白名單", + "infoAlert0": "白名單內網站的上網時長和打開次數不會被統計", + "infoAlert1": "白名單內網站的上網時間也不會被限製", + "placeholder": "網域", + "errorInput": "網域格式錯誤" + }, + "en": { + "addConfirmMsg": "{url} won't be counted after added into the whitelist any more.", + "removeConfirmMsg": "{url} will be removed from the whitelist.", + "duplicateMsg": "Duplicated", + "infoAlertTitle": "You can set a whitelist of sites in this page", + "infoAlert0": "Whitelisted sites will not be counted", + "infoAlert1": "Whitelisted sites will not be restricted", + "placeholder": "Site URL", + "errorInput": "Invalid site URL" + }, + "ja": { + "addConfirmMsg": "{url} がホワイトリストに追加されると、このWebサイトの統計はカウントされなくなります。", + "removeConfirmMsg": "{url} はホワイトリストから削除されます", + "duplicateMsg": "繰り返される", + "infoAlertTitle": "このページでサイトのホワイトリストを設定できます", + "infoAlert0": "ホワイトリストのサイトはカウントされません。", + "infoAlert1": "ホワイトリストのサイトは制限されません。", + "placeholder": "URL", + "errorInput": "無効なURL" + }, + "pt_PT": { + "addConfirmMsg": "{url} não será mais contado após adicionado à lista ignorada.", + "removeConfirmMsg": "{url} será removido da lista ignorada.", + "duplicateMsg": "Duplicado", + "infoAlertTitle": "Você pode definir uma lista de sites ignorados nesta página", + "infoAlert0": "Sites na lista ignorada não serão contados", + "infoAlert1": "Sites na lista ignorada não serão restritos", + "placeholder": "URL do Site", + "errorInput": "URL inválido do site" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/whitelist.ts b/src/i18n/message/app/whitelist.ts index 255f5091b..035daeff6 100644 --- a/src/i18n/message/app/whitelist.ts +++ b/src/i18n/message/app/whitelist.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './whitelist-resource.json' + export type WhitelistMessage = { addConfirmMsg: string removeConfirmMsg: string @@ -16,47 +18,6 @@ export type WhitelistMessage = { errorInput: string } -const _default: Messages = { - zh_CN: { - addConfirmMsg: '{url} 加入白名单后,将不再统计该网站的数据', - removeConfirmMsg: '{url} 将从白名单中移除', - duplicateMsg: '已存在白名单中', - infoAlertTitle: '你可以在这里配置网站白名单', - infoAlert0: '白名单内网站的上网时长和打开次数不会被统计', - infoAlert1: '白名单内网站的上网时间也不会被限制', - placeholder: '域名', - errorInput: '域名格式错误', - }, - zh_TW: { - addConfirmMsg: '{url} 加入白名單後,將不再統計該網站的數據', - removeConfirmMsg: '{url} 將從白名單中移除', - duplicateMsg: '已存在白名單中', - infoAlertTitle: '你可以在這裡配置網站白名單', - infoAlert0: '白名單內網站的上網時長和打開次數不會被統計', - infoAlert1: '白名單內網站的上網時間也不會被限製', - placeholder: '網域', - errorInput: '網域格式錯誤', - }, - en: { - addConfirmMsg: '{url} won\'t be counted after added into the whitelist any more.', - removeConfirmMsg: '{url} will be removed from the whitelist.', - duplicateMsg: 'Duplicated', - infoAlertTitle: 'You can set a whitelist of sites in this page', - infoAlert0: 'Whitelisted sites will not be counted', - infoAlert1: 'Whitelisted sites will not be restricted', - placeholder: 'Site URL', - errorInput: 'Invalid site URL', - }, - ja: { - addConfirmMsg: '{url} がホワイトリストに追加されると、このWebサイトの統計はカウントされなくなります。', - removeConfirmMsg: '{url} はホワイトリストから削除されます', - duplicateMsg: '繰り返される', - infoAlertTitle: 'このページでサイトのホワイトリストを設定できます', - infoAlert0: 'ホワイトリストのサイトはカウントされません。', - infoAlert1: 'ホワイトリストのサイトは制限されません。', - placeholder: 'URL', - errorInput: '無効なURL', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/base-resource.json b/src/i18n/message/common/base-resource.json new file mode 100644 index 000000000..012a7cdce --- /dev/null +++ b/src/i18n/message/common/base-resource.json @@ -0,0 +1,27 @@ +{ + "en": { + "currentVersion": "Version: {version}", + "allFunction": "All Functions", + "guidePage": "User Manual" + }, + "zh_CN": { + "currentVersion": "版本: v{version}", + "allFunction": "所有功能", + "guidePage": "用户手册" + }, + "zh_TW": { + "currentVersion": "版本: v{version}", + "allFunction": "所有功能", + "guidePage": "使用者手冊" + }, + "ja": { + "currentVersion": "バージョン: v{version}", + "allFunction": "すべての機能", + "guidePage": "ユーザーマニュアル" + }, + "pt_PT": { + "currentVersion": "Versão: {version}", + "allFunction": "Todas as Funções", + "guidePage": "Manual do Utilizador" + } +} \ No newline at end of file diff --git a/src/i18n/message/common/base.ts b/src/i18n/message/common/base.ts index 042b44eeb..4bc7c881e 100644 --- a/src/i18n/message/common/base.ts +++ b/src/i18n/message/common/base.ts @@ -1,10 +1,12 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import resource from './base-resource.json' + export type BaseMessage = { currentVersion: string allFunction: string @@ -14,27 +16,6 @@ export type BaseMessage = { /** * Use for chrome */ -const _default: Messages = { - en: { - currentVersion: 'Version: {version}', - allFunction: 'All Functions', - guidePage: 'User Manual', - }, - zh_CN: { - currentVersion: '版本: v{version}', - allFunction: '所有功能', - guidePage: '用户手册', - }, - zh_TW: { - currentVersion: '版本: v{version}', - allFunction: '所有功能', - guidePage: '用戶手冊', - }, - ja: { - currentVersion: 'バージョン: v{version}', - allFunction: 'すべての機能', - guidePage: 'ユーザーマニュアル', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/button-resource.json b/src/i18n/message/common/button-resource.json new file mode 100644 index 000000000..d8610a901 --- /dev/null +++ b/src/i18n/message/common/button-resource.json @@ -0,0 +1,62 @@ +{ + "en": { + "create": "New", + "delete": "Delete", + "modify": "Modify", + "save": "Save", + "test": "Test", + "paste": "Paste", + "confirm": "Confirm", + "cancel": "Cancel", + "okey": "OK", + "dont": "NO!" + }, + "zh_CN": { + "create": "新增", + "delete": "删除", + "modify": "修改", + "save": "保存", + "test": "测试", + "paste": "粘贴", + "confirm": "确认", + "cancel": "取消", + "okey": "好的", + "dont": "不用了" + }, + "zh_TW": { + "create": "新增", + "delete": "刪除", + "modify": "修改", + "save": "保存", + "test": "測試", + "paste": "粘貼", + "confirm": "確認", + "cancel": "取消", + "okey": "好的", + "dont": "不用了" + }, + "ja": { + "create": "追加", + "delete": "消去", + "modify": "変更", + "save": "保存", + "test": "試す", + "paste": "貼り付ける", + "confirm": "確かめる", + "cancel": "取り消す", + "okey": "OK", + "dont": "いいえ!" + }, + "pt_PT": { + "create": "Criar", + "delete": "Excluir", + "modify": "Modificar", + "save": "Salvar", + "test": "Ensaiar", + "paste": "Colar", + "confirm": "Confirmar", + "cancel": "Cancelar", + "okey": "OK", + "dont": "NÃO!" + } +} \ No newline at end of file diff --git a/src/i18n/message/common/button.ts b/src/i18n/message/common/button.ts new file mode 100644 index 000000000..0ff343dea --- /dev/null +++ b/src/i18n/message/common/button.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import resource from './button-resource.json' + +export type ButtonMessage = { + create: string + delete: string + modify: string + save: string + test: string + paste: string + confirm: string + cancel: string + okey: string + dont: string +} + +const _default: Messages = resource + +export default _default \ No newline at end of file diff --git a/src/i18n/message/common/calendar-resource.json b/src/i18n/message/common/calendar-resource.json new file mode 100644 index 000000000..8aa45a4f9 --- /dev/null +++ b/src/i18n/message/common/calendar-resource.json @@ -0,0 +1,107 @@ +{ + "zh_CN": { + "weekDays": "星期一|星期二|星期三|星期四|星期五|星期六|星期天", + "months": "一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月", + "dateFormat": "{y}/{m}/{d}", + "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}", + "label": { + "startDate": "开始日期", + "endDate": "结束日期" + }, + "range": { + "today": "今天", + "yesterday": "昨天", + "last24Hours": "最近 24 小时", + "last3Days": "最近 3 天", + "last7Days": "最近 7 天", + "last15Days": "最近 15 天", + "last30Days": "最近 30 天", + "last60Days": "最近 60 天", + "last90Days": "最近 90 天" + } + }, + "zh_TW": { + "weekDays": "禮拜一|禮拜二|禮拜三|禮拜四|禮拜五|禮拜六|禮拜天", + "months": "一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月", + "dateFormat": "{y}/{m}/{d}", + "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}", + "label": { + "startDate": "開始日期", + "endDate": "結束日期" + }, + "range": { + "today": "今天", + "yesterday": "昨天", + "last24Hours": "最近 24 小時", + "last3Days": "最近 3 天", + "last7Days": "最近 7 天", + "last15Days": "最近 15 天", + "last30Days": "最近 30 天", + "last60Days": "最近 60 天", + "last90Days": "最近 90 天" + } + }, + "en": { + "weekDays": "Mon|Tue|Wed|Thu|Fri|Sat|Sun", + "months": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec", + "dateFormat": "{m}/{d}/{y}", + "timeFormat": "{m}/{d}/{y} {h}:{i}:{s}", + "label": { + "startDate": "Start date", + "endDate": "End date" + }, + "range": { + "today": "Today", + "yesterday": "Yesterday", + "last24Hours": "Last 24 hours", + "last3Days": "Last 3 days", + "last7Days": "Last 7 days", + "last15Days": "Last 15 days", + "last30Days": "Last 30 days", + "last60Days": "Last 60 days", + "last90Days": "Last 90 days" + } + }, + "ja": { + "weekDays": "Mon|Tue|Wed|Thu|Fri|Sat|Sun", + "months": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec", + "dateFormat": "{y}/{m}/{d}", + "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}", + "label": { + "startDate": "開始日", + "endDate": "終了日" + }, + "range": { + "today": "今日", + "yesterday": "昨日", + "last24Hours": "過去 24 時間", + "last3Days": "過去 3 日間", + "last7Days": "過去 7 日間", + "last15Days": "過去 15 日間", + "last30Days": "過去 30 日間", + "last60Days": "過去 60 日間", + "last90Days": "過去 90 日間" + } + }, + "pt_PT": { + "weekDays": "Seg|Ter|Qua|Qui|Sex|Sab|Dom", + "months": "Jan|Fev|Mar|Abr|Maio|Jun|Jul|Ago|Set|Out|Nov|Dez", + "dateFormat": "{d}/{m}/{y}", + "timeFormat": "{d}/{m}/{y} {h}:{i}:{s}", + "label": { + "startDate": "Data de início", + "endDate": "Data de fim" + }, + "range": { + "today": "Hoje", + "yesterday": "Ontem", + "last3Days": "Últimos 3 dias", + "last7Days": "Últimos 7 dias", + "last15Days": "Últimos 15 dias", + "last30Days": "Últimos 30 dias", + "last60Days": "Últimos 60 dias", + "last90Days": "Últimos 90 dias", + "last24Hours": "Últimas 24 horas" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/common/calendar.ts b/src/i18n/message/common/calendar.ts index 46de6d195..44be68735 100644 --- a/src/i18n/message/common/calendar.ts +++ b/src/i18n/message/common/calendar.ts @@ -5,38 +5,30 @@ * https://opensource.org/licenses/MIT */ +import resource from './calendar-resource.json' + export type CalendarMessage = { weekDays: string months: string dateFormat: string timeFormat: string + label: { + startDate: string + endDate: string + } + range: { + today: string + yesterday: string + last24Hours: string + last3Days: string + last7Days: string + last15Days: string + last30Days: string + last60Days: string + last90Days: string + } } -const _default: Messages = { - zh_CN: { - weekDays: '星期一|星期二|星期三|星期四|星期五|星期六|星期天', - months: '一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月', - dateFormat: '{y}/{m}/{d}', - timeFormat: '{y}/{m}/{d} {h}:{i}:{s}', - }, - zh_TW: { - weekDays: '禮拜一|禮拜二|禮拜三|禮拜四|禮拜五|禮拜六|禮拜天', - months: '一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月', - dateFormat: '{y}/{m}/{d}', - timeFormat: '{y}/{m}/{d} {h}:{i}:{s}', - }, - en: { - weekDays: 'Mon|Tue|Wed|Thu|Fri|Sat|Sun', - months: 'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec', - dateFormat: '{m}/{d}/{y}', - timeFormat: '{m}/{d}/{y} {h}:{i}:{s}', - }, - ja: { - weekDays: 'Mon|Tue|Wed|Thu|Fri|Sat|Sun', - months: 'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec', - dateFormat: '{y}/{m}/{d}', - timeFormat: '{y}/{m}/{d} {h}:{i}:{s}', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/content-script-resource.json b/src/i18n/message/common/content-script-resource.json new file mode 100644 index 000000000..15fa5fade --- /dev/null +++ b/src/i18n/message/common/content-script-resource.json @@ -0,0 +1,47 @@ +{ + "zh_CN": { + "consoleLog": "今天您打开了 {time} 次 {host},花费了 {focus} 来浏览它。", + "closeAlert": "你可以在【网费很贵】的选项中关闭以上提示!", + "timeWithHour": "{hour} 小时 {minute} 分 {second} 秒", + "timeWithMinute": "{minute} 分 {second} 秒", + "timeWithSecond": "{second} 秒", + "timeLimitMsg": "您已被【{appName}】限制上网", + "more5Minutes": "再看 5 分钟!!我保证!" + }, + "zh_TW": { + "consoleLog": "今天您打開了 {time} 次 {host},花費了 {focus} 來瀏覽它。", + "closeAlert": "你可以在【{appName}】的選項中關閉以上提示!", + "timeWithHour": "{hour} 小時 {minute} 分 {second} 秒", + "timeWithMinute": "{minute} 分 {second} 秒", + "timeWithSecond": "{second} 秒", + "timeLimitMsg": "您已被【{appName}】限製上網", + "more5Minutes": "再看 5 分鐘!!我保証!" + }, + "en": { + "consoleLog": "You have open {host} for {time} time(s) and browsed it for {focus} today.", + "closeAlert": "You can turn off the above tips in the option of [{appName}]!", + "timeWithHour": "{hour} hour(s) {minute} minute(s) {second} second(s)", + "timeWithMinute": "{minute} minute(s) {second} second(s)", + "timeWithSecond": "{second} second(s)", + "timeLimitMsg": "You have been restricted by [{appName}]", + "more5Minutes": "More 5 minutes, please!!" + }, + "ja": { + "consoleLog": "{host} を {time} 回開いて、今日 {focus} をブラウズしました。", + "closeAlert": "{appName} のオプションで上記のヒントをオフにすることができます!", + "timeWithHour": "{hour} 時間 {minute} 分 {second} 秒", + "timeWithMinute": "{minute} 分 {second} 秒", + "timeWithSecond": "{second} 秒", + "timeLimitMsg": "【{appName}】によって制限されています", + "more5Minutes": "さらに5分間見てください! ! 約束します!" + }, + "pt_PT": { + "closeAlert": "Você pode desligar as dicas acima na opção do {appName}!", + "timeWithHour": "{hour} horas {minute} minutos {second} segundos", + "timeWithMinute": "{minute} minuto(s) {second} segundo(s)", + "timeWithSecond": "{second} segundo(s)", + "timeLimitMsg": "Foi restrito por [{appName}]", + "more5Minutes": "Mais 5 minutos, por favor!!", + "consoleLog": "Abriu {host} por {time} vez(es) e navegou por {focus} hoje." + } +} \ No newline at end of file diff --git a/src/i18n/message/common/content-script.ts b/src/i18n/message/common/content-script.ts index 457567bc8..5da6e3809 100644 --- a/src/i18n/message/common/content-script.ts +++ b/src/i18n/message/common/content-script.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './content-script-resource.json' + export type ContentScriptMessage = { consoleLog: string closeAlert: string @@ -15,43 +17,6 @@ export type ContentScriptMessage = { more5Minutes: string } -const _default: Messages = { - zh_CN: { - consoleLog: '今天您打开了 {time} 次 {host},花费了 {focus} 来浏览它。', - closeAlert: '你可以在【网费很贵】的选项中关闭以上提示!', - timeWithHour: '{hour} 小时 {minute} 分 {second} 秒', - timeWithMinute: '{minute} 分 {second} 秒', - timeWithSecond: '{second} 秒', - timeLimitMsg: '您已被【{appName}】限制上网', - more5Minutes: '再看 5 分钟!!我保证!', - }, - zh_TW: { - consoleLog: '今天您打開了 {time} 次 {host},花費了 {focus} 來瀏覽它。', - closeAlert: '你可以在【網費很貴】的選項中關閉以上提示!', - timeWithHour: '{hour} 小時 {minute} 分 {second} 秒', - timeWithMinute: '{minute} 分 {second} 秒', - timeWithSecond: '{second} 秒', - timeLimitMsg: '您已被【{appName}】限製上網', - more5Minutes: '再看 5 分鐘!!我保証!', - }, - en: { - consoleLog: 'You have open {host} for {time} time(s) and browsed it for {focus} today.', - closeAlert: 'You can turn off the above tips in the option of Timer!', - timeWithHour: '{hour} hour(s) {minute} minute(s) {second} second(s)', - timeWithMinute: '{minute} minute(s) {second} second(s)', - timeWithSecond: '{second} second(s)', - timeLimitMsg: 'You have been restricted by [{appName}]', - more5Minutes: 'More 5 minutes, please!!', - }, - ja: { - consoleLog: '{host} を {time} 回開いて、今日 {focus} をブラウズしました。', - closeAlert: 'Timer のオプションで上記のヒントをオフにすることができます!', - timeWithHour: '{hour} 時間 {minute} 分 {second} 秒', - timeWithMinute: '{minute} 分 {second} 秒', - timeWithSecond: '{second} 秒', - timeLimitMsg: '【{appName}】によって制限されています', - more5Minutes: 'さらに5分間見てください! ! 約束します!', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/context-menus-resource.json b/src/i18n/message/common/context-menus-resource.json new file mode 100644 index 000000000..c3811e7b8 --- /dev/null +++ b/src/i18n/message/common/context-menus-resource.json @@ -0,0 +1,37 @@ +{ + "zh_CN": { + "add2Whitelist": "将{host}加入白名单", + "removeFromWhitelist": "将{host}从白名单移出", + "optionPage": "扩展选项", + "repoPage": "源码下载", + "feedbackPage": "吐槽一下" + }, + "zh_TW": { + "add2Whitelist": "將 {host} 加入白名單", + "removeFromWhitelist": "將 {host} 從白名單移出", + "optionPage": "擴充選項", + "repoPage": "源碼下載", + "feedbackPage": "吐槽一下" + }, + "en": { + "add2Whitelist": "Add {host} to the whitelist", + "removeFromWhitelist": "Remove {host} from the whitelist", + "optionPage": "Options", + "repoPage": "Source Code", + "feedbackPage": "Issues" + }, + "ja": { + "add2Whitelist": "ホワイトリストに {host} を追加", + "removeFromWhitelist": "ホワイトリストから {host} を削除します", + "optionPage": "拡張設定", + "repoPage": "ソースコード", + "feedbackPage": "フィードバックの欠如" + }, + "pt_PT": { + "add2Whitelist": "Ignorar {host}", + "removeFromWhitelist": "Habilitar {host}", + "optionPage": "Opções", + "repoPage": "Código Fonte", + "feedbackPage": "Problemas" + } +} \ No newline at end of file diff --git a/src/i18n/message/common/context-menus.ts b/src/i18n/message/common/context-menus.ts index 09117dee4..43257605b 100644 --- a/src/i18n/message/common/context-menus.ts +++ b/src/i18n/message/common/context-menus.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './context-menus-resource.json' + /** * Used for menu */ @@ -16,35 +18,6 @@ export type ContextMenusMessage = { feedbackPage: string } -const _default: Messages = { - zh_CN: { - add2Whitelist: '将{host}加入白名单', - removeFromWhitelist: '将{host}从白名单移出', - optionPage: '扩展选项', - repoPage: '源码下载', - feedbackPage: '吐槽一下', - }, - zh_TW: { - add2Whitelist: '將{host}加入白名單', - removeFromWhitelist: '將{host}從白名單移出', - optionPage: '擴充選項', - repoPage: '源碼下載', - feedbackPage: '吐槽一下', - }, - en: { - add2Whitelist: 'Add {host} to the whitelist', - removeFromWhitelist: 'Remove {host} from the whitelist', - optionPage: 'Options', - repoPage: 'Source Code', - feedbackPage: 'Issues', - }, - ja: { - add2Whitelist: 'ホワイトリストに {host} を追加', - removeFromWhitelist: 'ホワイトリストから {host} を削除します', - optionPage: '拡張設定', - repoPage: 'ソースコード', - feedbackPage: 'フィードバックの欠如', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/initial-resource.json b/src/i18n/message/common/initial-resource.json new file mode 100644 index 000000000..3481c65b6 --- /dev/null +++ b/src/i18n/message/common/initial-resource.json @@ -0,0 +1,42 @@ +{ + "zh_CN": { + "localFile": { + "json": "JSON 文件", + "pdf": "PDF 文件", + "pic": "图片文件", + "txt": "文本文件" + } + }, + "zh_TW": { + "localFile": { + "json": "JSON 文件", + "pdf": "PDF 文件", + "pic": "圖片文件", + "txt": "文本文件" + } + }, + "en": { + "localFile": { + "json": "JSON Files", + "pdf": "PDF Files", + "pic": "Images", + "txt": "Text Files" + } + }, + "ja": { + "localFile": { + "json": "JSON", + "pdf": "PDF", + "pic": "写真", + "txt": "TXT" + } + }, + "pt_PT": { + "localFile": { + "json": "Arquivos JSON", + "pdf": "Arquivos PDF", + "pic": "Imagens", + "txt": "Arquivos de Texto" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/common/initial.ts b/src/i18n/message/common/initial.ts index ab1a7f613..71efca187 100644 --- a/src/i18n/message/common/initial.ts +++ b/src/i18n/message/common/initial.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './initial-resource.json' + /** * Locales for initial data * @@ -19,39 +21,6 @@ export type InitialMessage = { } } -const _default: Messages = { - zh_CN: { - localFile: { - json: 'JSON 文件', - pdf: 'PDF 文件', - pic: '图片文件', - txt: '文本文件', - }, - }, - zh_TW: { - localFile: { - json: 'JSON 文件', - pdf: 'PDF 文件', - pic: '圖片文件', - txt: '文本文件', - }, - }, - en: { - localFile: { - json: 'JSON Files', - pdf: 'PDF Files', - pic: 'Images', - txt: 'Text Files', - }, - }, - ja: { - localFile: { - json: 'JSON', - pdf: 'PDF', - pic: '写真', - txt: 'TXT', - }, - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/item-resource.json b/src/i18n/message/common/item-resource.json new file mode 100644 index 000000000..dbfad7db3 --- /dev/null +++ b/src/i18n/message/common/item-resource.json @@ -0,0 +1,92 @@ +{ + "zh_CN": { + "date": "日期", + "host": "域名", + "focus": "浏览时长", + "time": "打开次数", + "operation": { + "label": "操作", + "delete": "删除", + "add2Whitelist": "白名单", + "removeFromWhitelist": "启用", + "analysis": "分析", + "deleteConfirmMsgAll": "{url} 的所有访问记录将被删除", + "deleteConfirmMsgRange": "{url} 在 {start} 到 {end} 的访问记录将被删除", + "deleteConfirmMsg": "{url} 在 {date} 的访问记录将被删除", + "exportWholeData": "导出数据", + "importWholeData": "导入数据" + } + }, + "zh_TW": { + "date": "日期", + "host": "域名", + "focus": "瀏覽時長", + "time": "訪問次數", + "operation": { + "label": "操作", + "delete": "刪除", + "add2Whitelist": "白名單", + "removeFromWhitelist": "啟用", + "analysis": "分析", + "deleteConfirmMsgAll": "{url} 的所有拜訪記錄將被刪除", + "deleteConfirmMsgRange": "{url} 在 {start} 到 {end} 的拜訪記錄將被刪除", + "deleteConfirmMsg": "{url} 在 {date} 的拜訪記錄將被刪除", + "exportWholeData": "導出數據", + "importWholeData": "導入數據" + } + }, + "en": { + "date": "Date", + "host": "Site URL", + "focus": "Browsing Time", + "time": "Site Visits", + "operation": { + "label": "Operations", + "delete": "Delete", + "add2Whitelist": "Whitelist", + "removeFromWhitelist": "Enable", + "analysis": "Analysis", + "deleteConfirmMsgAll": "All records of {url} will be deleted!", + "deleteConfirmMsgRange": "All records of {url} between {start} and {end} will be deleted!", + "deleteConfirmMsg": "The record of {url} on {date} will be deleted!", + "exportWholeData": "Export Data", + "importWholeData": "Import Data" + } + }, + "ja": { + "date": "日期", + "host": "URL", + "focus": "閲覧時間", + "time": "拜訪回数", + "operation": { + "label": "操作", + "delete": "削除", + "add2Whitelist": "ホワイトリスト", + "removeFromWhitelist": "有効にする", + "analysis": "分析する", + "deleteConfirmMsgAll": "{url} のすべての拜訪記録が削除されます", + "deleteConfirmMsgRange": "{url} {start} から {end} までの拜訪記録は削除されます", + "deleteConfirmMsg": "{date} の {url} の拜訪記録は削除されます", + "exportWholeData": "インポート", + "importWholeData": "書き出す" + } + }, + "pt_PT": { + "date": "Data", + "host": "URL do Site", + "focus": "Tempo de Navegação", + "time": "Visitas do Site", + "operation": { + "label": "Operações", + "delete": "Apagar", + "add2Whitelist": "Ignorar", + "removeFromWhitelist": "Habilitar", + "deleteConfirmMsgAll": "Todos os registros de {url} serão apagados!", + "deleteConfirmMsgRange": "Todos os registros de {url} entre {start} e {end} serão apagados!", + "deleteConfirmMsg": "O registro de {url} em {date} será excluído!", + "exportWholeData": "Exportar Dados", + "importWholeData": "Importar Dados", + "analysis": "Análise" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/common/item.ts b/src/i18n/message/common/item.ts index c5355512c..7190f16a2 100644 --- a/src/i18n/message/common/item.ts +++ b/src/i18n/message/common/item.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './item-resource.json' + export type ItemMessage = { date: string host: string @@ -24,79 +26,6 @@ export type ItemMessage = { } } -const _default: Messages = { - zh_CN: { - date: '日期', - host: '域名', - focus: '浏览时长', - time: '打开次数', - operation: { - label: '操作', - delete: '删除', - add2Whitelist: '白名单', - removeFromWhitelist: '启用', - analysis: '分析', - deleteConfirmMsgAll: '{url} 的所有访问记录将被删除', - deleteConfirmMsgRange: '{url} 在 {start} 到 {end} 的访问记录将被删除', - deleteConfirmMsg: '{url} 在 {date} 的访问记录将被删除', - exportWholeData: '导出数据', - importWholeData: '导入数据', - }, - }, - zh_TW: { - date: '日期', - host: '域名', - focus: '瀏覽時長', - time: '訪問次數', - operation: { - label: '操作', - delete: '刪除', - add2Whitelist: '白名單', - removeFromWhitelist: '啟用', - analysis: '分析', - deleteConfirmMsgAll: '{url} 的所有拜訪記錄將被刪除', - deleteConfirmMsgRange: '{url} 在 {start} 到 {end} 的拜訪記錄將被刪除', - deleteConfirmMsg: '{url} 在 {date} 的拜訪記錄將被刪除', - exportWholeData: '導出數據', - importWholeData: '導入數據', - }, - }, - en: { - date: 'Date', - host: 'Site URL', - focus: 'Browsing Time', - time: 'Site Visits', - operation: { - label: 'Operations', - delete: 'Delete', - add2Whitelist: 'Whitelist', - removeFromWhitelist: 'Enable', - analysis: 'Analysis', - deleteConfirmMsgAll: 'All records of {url} will be deleted!', - deleteConfirmMsgRange: 'All records of {url} between {start} and {end} will be deleted!', - deleteConfirmMsg: 'The record of {url} on {date} will be deleted!', - exportWholeData: 'Export Data', - importWholeData: 'Import Data', - }, - }, - ja: { - date: '日期', - host: 'URL', - focus: '閲覧時間', - time: '拜訪回数', - operation: { - label: '操作', - delete: '削除', - add2Whitelist: 'ホワイトリスト', - removeFromWhitelist: '有効にする', - analysis: '分析する', - deleteConfirmMsgAll: '{url} のすべての拜訪記録が削除されます', - deleteConfirmMsgRange: '{url} {start} から {end} までの拜訪記録は削除されます', - deleteConfirmMsg: '{date} の {url} の拜訪記録は削除されます', - exportWholeData: 'インポート', - importWholeData: '書き出す', - }, - }, -} +const _default: Messages = resource export default _default diff --git a/src/i18n/message/common/locale-resource.json b/src/i18n/message/common/locale-resource.json new file mode 100644 index 000000000..10e83ad2f --- /dev/null +++ b/src/i18n/message/common/locale-resource.json @@ -0,0 +1,82 @@ +{ + "zh_CN": { + "name": "简体中文", + "comma": "," + }, + "zh_TW": { + "name": "正體中文", + "comma": "," + }, + "en": { + "name": "English", + "comma": ", " + }, + "ja": { + "name": "日本語", + "comma": "、" + }, + "pt_PT": { + "name": "Português", + "comma": "," + }, + "pl": { + "name": "Polski" + }, + "ko": { + "name": "한국인" + }, + "de": { + "name": "Deutsch" + }, + "es": { + "name": "Español" + }, + "ru": { + "name": "Русский" + }, + "uk": { + "name": "українська" + }, + "fr": { + "name": "Français" + }, + "it": { + "name": "italiano" + }, + "sv": { + "name": "Sverige" + }, + "fi": { + "name": "Suomalainen" + }, + "da": { + "name": "dansk" + }, + "hr": { + "name": "Hrvatski" + }, + "id": { + "name": "bahasa Indonesia" + }, + "tr": { + "name": "Türkçe" + }, + "cs": { + "name": "čeština" + }, + "ro": { + "name": "Română" + }, + "nl": { + "name": "Nederlands" + }, + "vi": { + "name": "Tiếng Việt" + }, + "sk": { + "name": "slovenský" + }, + "mn": { + "name": "Монгол" + } +} \ No newline at end of file diff --git a/src/i18n/message/common/locale.ts b/src/i18n/message/common/locale.ts index 135fa455d..ec1d4045a 100644 --- a/src/i18n/message/common/locale.ts +++ b/src/i18n/message/common/locale.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './locale-resource.json' + type MetaBase = { name: string } @@ -25,89 +27,6 @@ export type LocaleMessages = [translatingLocale in timer.TranslatingLocale]: MetaBase } -const _default: LocaleMessages = { - zh_CN: { - name: '简体中文', - comma: ',' - }, - zh_TW: { - name: '正體中文', - comma: ',', - }, - en: { - name: 'English', - comma: ', ' - }, - ja: { - name: '日本語', - comma: '、' - }, - pl: { - name: 'Polski' - }, - pt: { - name: 'Português' - }, - pt_BR: { - name: 'Portugues, Brasil' - }, - ko: { - name: '한국인' - }, - de: { - name: 'Deutsch' - }, - es: { - name: 'Español' - }, - ru: { - name: 'Русский' - }, - uk: { - name: "українська" - }, - fr: { - name: "Français" - }, - it: { - name: "italiano" - }, - sv: { - name: "Sverige" - }, - fi: { - name: "Suomalainen", - }, - da: { - name: "dansk", - }, - hr: { - name: "Hrvatski", - }, - id: { - name: "bahasa Indonesia", - }, - tr: { - name: "Türkçe", - }, - cs: { - name: "čeština", - }, - ro: { - name: "Română", - }, - nl: { - name: "Nederlands", - }, - vi: { - name: "Tiếng Việt", - }, - sk: { - name: "slovenský", - }, - mn: { - name: "Монгол", - }, -} +const _default: LocaleMessages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/merge-resource.json b/src/i18n/message/common/merge-resource.json new file mode 100644 index 000000000..be8179dad --- /dev/null +++ b/src/i18n/message/common/merge-resource.json @@ -0,0 +1,32 @@ +{ + "en": { + "tagResult": { + "blank": "Not Merge", + "level": "Keep Level {level}" + } + }, + "zh_CN": { + "tagResult": { + "blank": "不合并", + "level": "{level} 级域名" + } + }, + "zh_TW": { + "tagResult": { + "blank": "不合並", + "level": "{level} 級網域" + } + }, + "ja": { + "tagResult": { + "blank": "不合并", + "level": "{level} 次ドメイン" + } + }, + "pt_PT": { + "tagResult": { + "blank": "Não Mesclar", + "level": "Mantenha o Nível {level}" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/common/merge.ts b/src/i18n/message/common/merge.ts new file mode 100644 index 000000000..3f056d4b4 --- /dev/null +++ b/src/i18n/message/common/merge.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type MergeCommonMessage = { + tagResult: { + blank: string + level: string + } +} + +const messages: Messages = { + en: { + tagResult: { + blank: 'Not Merge', + level: 'Keep Level {level}', + } + }, + zh_CN: { + tagResult: { + blank: '不合并', + level: '{level} 级域名', + }, + }, + zh_TW: { + tagResult: { + blank: '不合並', + level: '{level} 級網域', + } + }, + ja: { + tagResult: { + blank: '不合并', + level: '{level} 次ドメイン', + } + } +} + +export default messages \ No newline at end of file diff --git a/src/i18n/message/common/meta-resource.json b/src/i18n/message/common/meta-resource.json new file mode 100644 index 000000000..0ac02eeff --- /dev/null +++ b/src/i18n/message/common/meta-resource.json @@ -0,0 +1,29 @@ +{ + "zh_CN": { + "name": "网费很贵", + "marketName": "网费很贵 - 上网时间统计", + "description": "做最好用的上网时间统计工具。", + "slogan": "Insight & Improve" + }, + "zh_TW": { + "name": "網費很貴", + "marketName": "網費很貴 - 上網時間統計", + "description": "做最好用的上網時間統計工具。" + }, + "ja": { + "name": "Web時間統計", + "marketName": "Web時間統計", + "description": "最高のウェブタイムトラッカーになるために。" + }, + "en": { + "name": "Time Tracker", + "marketName": "Time Tracker", + "description": "To be the BEST webtime tracker.", + "slogan": "Insight & Improve" + }, + "pt_PT": { + "name": "Rastreador de Tempo", + "marketName": "Rastreador de Tempo", + "description": "Ser o MELHOR rastreador de tempo da web." + } +} \ No newline at end of file diff --git a/src/i18n/message/common/meta.ts b/src/i18n/message/common/meta.ts index 43b9b2b0e..89d9d29e3 100644 --- a/src/i18n/message/common/meta.ts +++ b/src/i18n/message/common/meta.ts @@ -1,31 +1,19 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import resource from './meta-resource.json' export type MetaMessage = { name: string marketName: string description: string + slogan: string } -const _default: Messages = { - zh_CN: { - name: '网费很贵', - marketName: '网费很贵 - 上网时间统计', - description: '做最好用的上网时间统计工具。', - }, - zh_TW: { - name: '網費很貴', - marketName: '網費很貴 - 上網時間統計', - description: '做最好用的上網時間統計工具。', - }, - ja: { - name: 'Web時間統計', - marketName: 'Web時間統計', - description: '最高のオンライン時間統計ツールを作成します。', - }, - en: { - name: 'Timer', - marketName: 'Timer - Browsing Time & Visit count', - description: 'To be the BEST web timer.', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/popup-duration-resource.json b/src/i18n/message/common/popup-duration-resource.json new file mode 100644 index 000000000..14116abb8 --- /dev/null +++ b/src/i18n/message/common/popup-duration-resource.json @@ -0,0 +1,32 @@ +{ + "zh_CN": { + "today": "今日", + "thisWeek": "本周", + "thisMonth": "本月", + "last30Days": "近 30 天" + }, + "zh_TW": { + "today": "今日", + "thisWeek": "本週", + "thisMonth": "本月", + "last30Days": "近 30 天" + }, + "en": { + "today": "Today's", + "thisWeek": "This Week's", + "thisMonth": "This Month's", + "last30Days": "Last 30 Days'" + }, + "ja": { + "today": "今日の", + "thisWeek": "今週の", + "thisMonth": "今月の", + "last30Days": "過去 30 日間" + }, + "pt_PT": { + "today": "Hoje", + "thisWeek": "Esta Semana", + "thisMonth": "Este Mês", + "last30Days": "Últimos 30 dias" + } +} \ No newline at end of file diff --git a/src/i18n/message/common/popup-duration.ts b/src/i18n/message/common/popup-duration.ts index a79846ba7..b91145441 100644 --- a/src/i18n/message/common/popup-duration.ts +++ b/src/i18n/message/common/popup-duration.ts @@ -5,33 +5,10 @@ * https://opensource.org/licenses/MIT */ +import resource from './popup-duration-resource.json' + export type PopupDurationMessage = { [key in timer.option.PopupDuration]: string } -const _default: Messages = { - zh_CN: { - today: '今日', - thisWeek: '本周', - thisMonth: '本月', - last30Days: '近 30 天', - }, - zh_TW: { - today: '今日', - thisWeek: '本週', - thisMonth: '本月', - last30Days: '近 30 天', - }, - en: { - today: 'Today\'s', - thisWeek: 'This Week\'s', - thisMonth: 'This Month\'s', - last30Days: 'Last 30 days\'', - }, - ja: { - today: '今日の', - thisWeek: '今週の', - thisMonth: '今月の', - last30Days: '過去 30 日間', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/app-resource.json b/src/i18n/message/guide/app-resource.json new file mode 100644 index 000000000..cc60bfd64 --- /dev/null +++ b/src/i18n/message/guide/app-resource.json @@ -0,0 +1,37 @@ +{ + "en": { + "title": "Enter the management page", + "p1": "Based on icons, the extension provides a more convenient way to view data. But if you want to experience full functionality, you need to visit the management page of the extension, via one of the following two ways.", + "l1": "You can right-click the icon of the extension, and click [{button}] in the pop-up menu.", + "l2": "You can also find the [{button}] link at the bottom of the icon popup page, just click it.", + "p2": "The popup page and management page are the main interaction methods of this extension. After you know how to open them, you can use it completely." + }, + "zh_CN": { + "title": "进入后台管理页面", + "p1": "基于图标,扩展提供了比较便捷的数据查看方式。但是如果您想要体验它的全部功能,就需要访问扩展的后台管理页。进入后台页有以下两种方式。", + "l1": "您可以右击扩展的图标,在弹出的菜单中点击【{button}】。", + "l2": "您也可以在图标弹出页的页脚找到【{button}】链接,同样地,点击它即可。", + "p2": "弹出页和后台页是这个扩展最主要的交互方式,当你知道如何打开他们之后,就可以完整地使用它了。" + }, + "zh_TW": { + "title": "進入管理頁面", + "p1": "基於圖標,擴展提供了比較便捷的數據查看方式。但是如果您想要體驗它的全部功能,就需要訪問擴展的後台管理頁。進入後台頁有以下兩種方式。", + "l1": "您可以右擊擴展的圖標,在彈出的菜單中點擊【{button}】。", + "l2": "您也可以在圖標彈出頁的頁腳找到【{button}】鏈接,同樣地,點擊它即可。", + "p2": "彈出頁和後台頁是這個擴展最主要的交互方式,當你知道如何打開他們之後,就可以完整地使用它了。" + }, + "ja": { + "title": "管理ページに入る", + "p1": "アイコンに基づいて、拡張機能はデータを表示するためのより便利な方法を提供します。 ただし、その完全な機能を体験したい場合は、拡張バックグラウンド管理ページにアクセスする必要があります. バックグラウンド ページに入る方法は 2 つあります。", + "l1": "拡張機能のアイコンを右クリックして、ポップアップ メニューの [{button}] をクリックします。", + "l2": "アイコン ポップアップ ページのフッターに [{button}] リンクがあり、同じ方法でクリックすることもできます。", + "p2": "ポップアップ ページと背景ページは、この拡張機能の主な対話方法であり、それらを開く方法を理解すれば、完全に使用できます。" + }, + "pt_PT": { + "title": "Digite a página de gestão", + "p1": "Com base nos ícones, a extensão fornece uma maneira mais conveniente de ver dados. Mas se quiser experimentar todas as funcionalidades, precisa visitar a página de gestão da extensão, por uma das duas seguintes maneiras.", + "l1": "Pode clicar com o botão direito do rato no ícone da extensão e clicar em [{button}] no ementa pop-up.", + "l2": "Também pode encontrar a ligação [{button}] na parte inferior da página pop-up de ícone, basta clicar nele.", + "p2": "A página pop-up e a página de gestão são os principais métodos de interação desta extensão. Após saber como abri-los, pode usá-lo completamente." + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/app.ts b/src/i18n/message/guide/app.ts new file mode 100644 index 000000000..9bfd49ddf --- /dev/null +++ b/src/i18n/message/guide/app.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import resource from './app-resource.json' + +export type AppMessage = { + title: string + p1: string + l1: string + l2: string + p2: string +} + +const _default: Messages = resource + +export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/backup-resource.json b/src/i18n/message/guide/backup-resource.json new file mode 100644 index 000000000..c4e8a5063 --- /dev/null +++ b/src/i18n/message/guide/backup-resource.json @@ -0,0 +1,97 @@ +{ + "en": { + "title": "Backup data with GitHub Gist", + "p1": "This extension supports users to backup data using GitHub Gist{link} through simple settings.", + "upload": { + "title": "Four simple steps to complete the setup", + "prepareToken": "First, generate a token with gist permissions on GitHub{link}.", + "enter": "Enter the option page{link}.", + "form": "Then select GitHub Gist as the synchronization method, and fill in your token in the input box that appears below.", + "backup": "Click the Backup button to upload local data to your GitHub Gist." + }, + "query": { + "title": "How to query data backed up by other browsers?", + "p1": "If you correctly set the token in the above steps, you can query remote data in just three simple steps.", + "enter": "First, enter the management page{link}, click the menu item {menuItem}.", + "enable": "If the token is set correctly, an icon, like {icon}, will appear in the upper right corner of the page, click it to enable the remote query.", + "wait": "Wait for the data query to complete, and move the mouse over the value to view the data of each client.", + "tip": "Because remote data is stored in monthly shards, the query time period should not be too long." + } + }, + "zh_CN": { + "title": "使用 GitHub Gist 备份数据", + "p1": "这个扩展支持用户通过简单的设置,使用 GitHub Gist{link} 备份数据。", + "upload": { + "title": "简单四步完成设置", + "prepareToken": "首先,您需要在 GitHub 生成一个包含 gist 权限的 token{link}。", + "enter": "进入扩展的选项页面{link}。", + "form": "然后将同步方式选为 GitHub Gist,将你的 token 填入下方出现的输入框中。", + "backup": "最后,点击备份按钮即可将本地数据导入到你的 gist 里。" + }, + "query": { + "title": "如何查询其他浏览器备份的数据?", + "p1": "如果您在上述步骤中正确设置了 token,只需简单三步即可查询远端数据。", + "enter": "首先,进入管理页{link},点击菜单项【{menuItem}】。", + "enable": "如果 token 设置正确,页面右上角会出现一个{icon}图标,点击它即可开启远端查询。", + "wait": "等待数据查询完毕,将鼠标移动到数值上,即可查看每个客户端的数据。", + "tip": "因为远端数据时按月份分片存放,所以查询时间段不宜过长。" + } + }, + "zh_TW": { + "title": "使用 GitHub Gist 備份數據", + "p1": "這個擴展支持用戶通過簡單的設置,使用 GitHub Gist{link} 備份數據。", + "upload": { + "title": "簡單四步完成設置", + "prepareToken": "首先,您需要在 GitHub 生成一個包含 gist 權限的 token{link}。", + "enter": "進入擴展的選項頁面{link}。", + "form": "然後將同步方式選為 GitHub Gist,將你的 token 填入下方出現的輸入框中。", + "backup": "最後,點擊備份按鈕即可將本地數據導入到你的 gist 裡。" + }, + "query": { + "title": "如何查詢其他瀏覽器備份的數據?", + "p1": "如果您在上述步驟中正確設置了 token,只需簡單三步即可查詢遠端數據。", + "enter": "首先,進入管理頁{link},點擊菜單項【{menuItem}】。", + "enable": "如果 token 設置正確,頁面右上角會出現一個 {icon} 圖標,點擊它即可開啟遠端查詢。", + "wait": "等待數據查詢完畢,將鼠標移動到數值上,即可查看每個客戶端的數據。", + "tip": "因為遠端數據時按月份分片存放,所以查詢時間段不宜過長。" + } + }, + "ja": { + "title": "GitHub Gist でデータをバックアップする", + "p1": "この拡張機能は、簡単な設定で GitHub Gist{link} を使用してデータをバックアップするユーザーをサポートします。", + "upload": { + "title": "セットアップを完了するための 4 つの簡単なステップ", + "prepareToken": "まず、GitHub{link} で Gist 権限を持つトークンを生成します。", + "enter": "オプションページ {link} に入ります。", + "form": "次に、同期方法として GitHub Gist を選択し、下に表示される入力ボックスにトークンを入力します。", + "backup": "[バックアップ] ボタンをクリックして、ローカル データを GitHub Gist にアップロードします。" + }, + "query": { + "title": "他のブラウザでバックアップされたデータを照会する方法は?", + "p1": "上記の手順でトークンを正しく設定すると、わずか 3 つの簡単な手順でリモート データをクエリできます。", + "enter": "まず、管理ページ {link} に入り、メニュー項目 {menuItem} をクリックします。", + "enable": "トークンが正しく設定されている場合、{icon} のようなアイコンがページの右上隅に表示されるので、それをクリックしてリモート クエリを有効にします。", + "wait": "データ クエリが完了するのを待ち、値の上にマウスを移動して、各クライアントのデータを表示します。", + "tip": "リモート データは毎月のシャードに保存されるため、クエリ期間が長すぎないようにする必要があります。" + } + }, + "pt_PT": { + "title": "Backup dos dados com GitHub Gist", + "p1": "Esta extensão permite aos utilizadores fazer backup de dados usando o GitHub Gist{link} através de configurações simples.", + "upload": { + "title": "Quatro passos simples para concluir a instalação", + "prepareToken": "Primeiro, gere um token com permissões gist no GitHub{link}.", + "enter": "Digite a página de opções{link}.", + "form": "Em seguida, selecione GitHub Gist como o método de sincronização e preencha seu token na caixa de entrada que aparece abaixo.", + "backup": "Clique no botão Backup para carregar os dados locais para o seu GitHub Gist." + }, + "query": { + "title": "Como realizar o backup de dados feito por outros navegadores?", + "p1": "Se você definir corretamente o token nos passos acima, poderá consultar dados remotos em apenas três etapas simples.", + "enter": "Primeiro, digite a página de gestão{link}, clique no item de menu {menuItem}.", + "enable": "Se o token estiver definido corretamente, um ícone, como {icon}, aparecerá no canto superior direito da página, clique nele para ativar a consulta remota.", + "wait": "Aguarde a conclusão da consulta de dados e mova o rato sobre o valor para ver os dados de cada cliente.", + "tip": "Como os dados remotos são armazenados em fragmentos mensais, o período de consulta não deve ser muito longo." + } + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/backup.ts b/src/i18n/message/guide/backup.ts new file mode 100644 index 000000000..46f6bb1a1 --- /dev/null +++ b/src/i18n/message/guide/backup.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import resource from './backup-resource.json' + +export type BackupMessage = { + title: String + p1: string + upload: { + title: string + prepareToken: string + enter: string + form: string + backup: string + } + query: { + title: string + p1: string + enter: string + enable: string + wait: string + tip: string + } +} + +const _default: Messages = resource + +export default _default diff --git a/src/i18n/message/guide/home-resource.json b/src/i18n/message/guide/home-resource.json new file mode 100644 index 000000000..c9300c1dd --- /dev/null +++ b/src/i18n/message/guide/home-resource.json @@ -0,0 +1,27 @@ +{ + "en": { + "desc": "{appName} can help you track the time you spent on browsing websites and the count of visit, with what you can insight and improve your web habits.", + "button": "Start Now!", + "download": "Install for {browser}" + }, + "zh_CN": { + "desc": "{appName}是一个开源、免费的上网时间统计插件。它可以帮助您统计每天在每个网站上所花费的时间和访问次数。您可以借此来观察您的上网习惯,并通过为指定网站设置每天的浏览上限来改善它。", + "button": "如何使用", + "download": "在 {browser} 上安装" + }, + "zh_TW": { + "desc": "{appName}是一個開源、免費的上網時間統計插件。它可以幫助您統計每天在每個網站上所花費的時間和訪問次數。您可以藉此來觀察您的上網習慣,並通過為指定網站設置每天的瀏覽上限來改善它。", + "button": "如何使用", + "download": "在 {browser} 上安裝" + }, + "ja": { + "desc": "{appName}は、ウェブサイトの閲覧に費やした時間と訪問回数を追跡するのに役立ちます。これにより、ウェブ習慣を洞察して改善することができます。", + "button": "すぐに始めましょう", + "download": "{browser} にインストール" + }, + "pt_PT": { + "desc": "O {appName} pode ajudá-lo a controlar o tempo gasto em sites de navegação e a contagem de visitas, com o que pode perceber e melhorar os seus hábitos na web.", + "button": "Comece agora!", + "download": "Instalar para {browser}" + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/home.ts b/src/i18n/message/guide/home.ts new file mode 100644 index 000000000..5b3d881e8 --- /dev/null +++ b/src/i18n/message/guide/home.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import resource from './home-resource.json' + +type _Key = + | 'desc' + | 'button' + | 'download' + +export type HomeMessage = { + [key in _Key]: string +} + +const _default: Messages = resource + +export default _default diff --git a/src/i18n/message/guide/index.ts b/src/i18n/message/guide/index.ts index 399d156aa..72463d00c 100644 --- a/src/i18n/message/guide/index.ts +++ b/src/i18n/message/guide/index.ts @@ -1,58 +1,115 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ import layoutMessages, { LayoutMessage } from "./layout" -import profileMessages, { ProfileMessage } from "./profile" -import usageMessages, { UsageMessage } from "./usage" +import startMessages, { StartMessage } from "./start" import privacyMessages, { PrivacyMessage } from "./privacy" import metaMessages, { MetaMessage } from "../common/meta" import baseMessages, { BaseMessage } from "../common/base" +import homeMessages, { HomeMessage } from "./home" +import appMessages, { AppMessage } from "./app" +import mergeMessages, { MergeMessage } from "./merge" +import virtualMessages, { VirtualMessage } from "./virtual" +import limitMessages, { LimitMessage } from "./limit" +import backupMessages, { BackupMessage } from "./backup" +import mergeCommonMessages, { MergeCommonMessage } from "../common/merge" +import appMenuMessages, { MenuMessage as AppMenuMessage } from "../app/menu" export type GuideMessage = { + mergeCommon: MergeCommonMessage layout: LayoutMessage - profile: ProfileMessage - usage: UsageMessage + home: HomeMessage + start: StartMessage privacy: PrivacyMessage meta: MetaMessage base: BaseMessage + app: AppMessage + merge: MergeMessage + virtual: VirtualMessage + limit: LimitMessage + backup: BackupMessage + appMenu: AppMenuMessage } const _default: Messages = { zh_CN: { + mergeCommon: mergeCommonMessages.zh_CN, layout: layoutMessages.zh_CN, - profile: profileMessages.zh_CN, - usage: usageMessages.zh_CN, + home: homeMessages.zh_CN, + start: startMessages.zh_CN, privacy: privacyMessages.zh_CN, meta: metaMessages.zh_CN, base: baseMessages.zh_CN, + app: appMessages.zh_CN, + merge: mergeMessages.zh_CN, + virtual: virtualMessages.zh_CN, + limit: limitMessages.zh_CN, + backup: backupMessages.zh_CN, + appMenu: appMenuMessages.zh_CN, }, zh_TW: { + mergeCommon: mergeCommonMessages.zh_TW, layout: layoutMessages.zh_TW, - profile: profileMessages.zh_TW, - usage: usageMessages.zh_TW, + home: homeMessages.zh_TW, + start: startMessages.zh_TW, privacy: privacyMessages.zh_TW, meta: metaMessages.zh_TW, base: baseMessages.zh_TW, + app: appMessages.zh_TW, + merge: mergeMessages.zh_TW, + virtual: virtualMessages.zh_TW, + limit: limitMessages.zh_TW, + backup: backupMessages.zh_TW, + appMenu: appMenuMessages.zh_TW, }, en: { + mergeCommon: mergeCommonMessages.en, layout: layoutMessages.en, - profile: profileMessages.en, - usage: usageMessages.en, + home: homeMessages.en, + start: startMessages.en, privacy: privacyMessages.en, meta: metaMessages.en, base: baseMessages.en, + app: appMessages.en, + merge: mergeMessages.en, + virtual: virtualMessages.en, + limit: limitMessages.en, + backup: backupMessages.en, + appMenu: appMenuMessages.en, }, ja: { + mergeCommon: mergeCommonMessages.ja, layout: layoutMessages.ja, - profile: profileMessages.ja, - usage: usageMessages.ja, + home: homeMessages.ja, + start: startMessages.ja, privacy: privacyMessages.ja, meta: metaMessages.ja, base: baseMessages.ja, + app: appMessages.ja, + merge: mergeMessages.ja, + virtual: virtualMessages.ja, + limit: limitMessages.ja, + backup: backupMessages.ja, + appMenu: appMenuMessages.ja, + }, + pt_PT: { + mergeCommon: mergeCommonMessages.pt_PT, + layout: layoutMessages.pt_PT, + home: homeMessages.pt_PT, + start: startMessages.pt_PT, + privacy: privacyMessages.pt_PT, + meta: metaMessages.pt_PT, + base: baseMessages.pt_PT, + app: appMessages.pt_PT, + merge: mergeMessages.pt_PT, + virtual: virtualMessages.pt_PT, + limit: limitMessages.pt_PT, + backup: backupMessages.pt_PT, + appMenu: appMenuMessages.pt_PT, }, } diff --git a/src/i18n/message/guide/layout-resource.json b/src/i18n/message/guide/layout-resource.json new file mode 100644 index 000000000..974e389f0 --- /dev/null +++ b/src/i18n/message/guide/layout-resource.json @@ -0,0 +1,47 @@ +{ + "zh_CN": { + "header": { + "sourceCode": "查看源代码", + "email": "联系作者" + }, + "menu": { + "usage": "高级用法" + } + }, + "zh_TW": { + "header": { + "sourceCode": "查看源代碼", + "email": "聯繫作者" + }, + "menu": { + "usage": "高級用法" + } + }, + "en": { + "header": { + "sourceCode": "View source code", + "email": "Contact author" + }, + "menu": { + "usage": "Advanced usages" + } + }, + "ja": { + "header": { + "sourceCode": "ソースコードを見る", + "email": "著者に連絡する" + }, + "menu": { + "usage": "高度な使い方" + } + }, + "pt_PT": { + "header": { + "sourceCode": "Exibir código-fonte", + "email": "Autor do contacto" + }, + "menu": { + "usage": "Uso avançado" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/layout.ts b/src/i18n/message/guide/layout.ts index 4be2cc2ac..9a1302636 100644 --- a/src/i18n/message/guide/layout.ts +++ b/src/i18n/message/guide/layout.ts @@ -1,97 +1,22 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import resource from './layout-resource.json' + export type LayoutMessage = { + header: { + sourceCode: string + email: string + } menu: { - profile: string - usage: { - title: string - quickstart: string - background: string - advanced: string - backup: string - } - privacy: { - title: string - scope: string - storage: string - } + usage: string } } -const _default: Messages = { - zh_CN: { - menu: { - profile: '欢迎安装{appName}', - usage: { - title: '如何使用', - quickstart: '快速开始', - background: '访问后台页面', - advanced: '高级功能', - backup: '使用 Gist 备份数据', - }, - privacy: { - title: '隐私声明', - scope: '收集哪些数据', - storage: '如何处理这些数据', - }, - }, - }, - zh_TW: { - menu: { - profile: '歡迎安裝{appName}', - usage: { - title: '如何使用', - quickstart: '快速開始', - background: '訪問後台頁面', - advanced: '高級功能', - backup: '使用 Gist 備份數據', - }, - privacy: { - title: '隱私聲明', - scope: '收集哪些數據', - storage: '如何處理這些數據', - }, - }, - }, - en: { - menu: { - profile: 'Welcome to install {appName}', - usage: { - title: 'Using Timer', - quickstart: 'Quickstart', - background: 'Using all functions', - advanced: 'Advanced features', - backup: 'Backup your data with Gist', - }, - privacy: { - title: 'Privary Policy', - scope: 'Personal data collected', - storage: 'How to do with this data', - }, - }, - }, - ja: { - menu: { - profile: '{appName}へようこそ', - usage: { - title: '使い方', - quickstart: 'クイックスタート', - background: 'すべての機能', - advanced: '高度な機能', - backup: 'Gist でデータをバックアップ', - }, - privacy: { - title: 'ポリシーと規約', - scope: '収集する情報', - storage: 'このデータをどうするか', - }, - }, - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/limit-resource.json b/src/i18n/message/guide/limit-resource.json new file mode 100644 index 000000000..06a1a71c7 --- /dev/null +++ b/src/i18n/message/guide/limit-resource.json @@ -0,0 +1,57 @@ +{ + "en": { + "title": "Limit browsing time of everyday", + "p1": "If you want to limit the time of browsing certain URLs each day, you can do so by creating a daily time limit rule.", + "step": { + "title": "Four steps to create a limit rule", + "enter": "First, enter the management page{link}, click the menu item {menuItem}.", + "click": "Click the New button in the upper right corner.", + "form": "Paste the URL that needs to be restricted, and the duration of the restriction. Some URL fragments can be replaced with wildcards as needed, or deleted directly. Then click Save.", + "check": "Finally, check whether the target URL hits the newly added rule through the Test button in the upper right corner." + } + }, + "zh_CN": { + "title": "限制每天的浏览时间", + "p1": "如果你想限制每天浏览某些 URL 的时长,可以通过创建每日时限规则来完成。", + "step": { + "title": "简单四步创建一个限制规则", + "enter": "首先进入管理页{link},点击菜单项【{menuItem}】。", + "click": "然后单击右上角的新建按钮。", + "form": "粘贴需要限制的 URL,以及限制时长。可以根据需要将部分 URL 片段使用通配符代替,或者直接删除。然后点击保存。", + "check": "最后通过右上角的测试功能检查目标 URL 是否命中了刚添加的规则。" + } + }, + "zh_TW": { + "title": "限制每天的瀏覽時間", + "p1": "如果你想限制每天瀏覽某些 URL 的時長,可以通過創建每日時限規則來完成。", + "step": { + "title": "簡單四步創建一個限制規則", + "enter": "首先進入管理頁{link},點擊菜單項【{menuItem}】。", + "click": "然後單擊右上角的新建按鈕。", + "form": "粘貼需要限制的 URL,以及限制時長。可以根據需要將部分 URL 片段使用通配符代替,或者直接刪除。然後點擊保存。", + "check": "最後通過右上角的測試功能檢查目標 URL 是否命中了剛添加的規則。" + } + }, + "ja": { + "title": "毎日の閲覧時間を制限する", + "p1": "特定の URL を毎日閲覧する時間を制限したい場合は、毎日の時間制限ルールを作成することでこれを行うことができます。", + "step": { + "title": "制限ルールを作成するための 4 つのステップ", + "enter": "まず、管理ページ {link} に入り、メニュー項目{menuItem} をクリックします。", + "click": "右上隅にある [新規] ボタンをクリックします。", + "form": "制限する必要がある URL と制限の期間を貼り付けます。一部の URL フラグメントは、必要に応じてワイルドカードに置き換えたり、直接削除したりできます。 次に、[保存] をクリックします。", + "check": "最後に、右上隅の [テスト] ボタンを使用して、ターゲット URL が新しく追加されたルールに一致するかどうかを確認します。" + } + }, + "pt_PT": { + "title": "Limitar tempo de navegação de todos os dias", + "p1": "Se deseja limitar o tempo de navegação de certas URL por dia, pode fazê-lo criando uma regra diária de limite de tempo.", + "step": { + "title": "Quatro passos para criar uma regra limite", + "enter": "Primeiro, digite a página de gestão{link}, clique no item de menu {menuItem}.", + "click": "Clique no botão \"Criar\" no canto superior direito.", + "form": "Cole a URL que precisa ser restrita, e a duração da restrição. Alguns fragmentos de URL podem ser substituídos por carácter curinga, conforme necessário, ou excluídos diretamente. Depois clique em Salvar.", + "check": "Finalmente, verifique se a URL de destino atinge a regra recém-adicionada através do botão Testar no canto superior direito." + } + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/limit.ts b/src/i18n/message/guide/limit.ts new file mode 100644 index 000000000..3325ee1b9 --- /dev/null +++ b/src/i18n/message/guide/limit.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import resource from './limit-resource.json' + +export type LimitMessage = { + title: string + p1: string + step: { + title: string + enter: string + click: string + form: string + check: string + } +} + +const _default: Messages = resource + +export default _default diff --git a/src/i18n/message/guide/merge-resource.json b/src/i18n/message/guide/merge-resource.json new file mode 100644 index 000000000..239b67bde --- /dev/null +++ b/src/i18n/message/guide/merge-resource.json @@ -0,0 +1,162 @@ +{ + "en": { + "title": "Summarize data of related sites", + "p1": "This extension is counted by domain name, for example, {demo1} and {demo2} will be counted as 2 records. If you want to see aggregated data for both sites, you'll need to use the merge feature.", + "p2": "On most data display pages, merged queries are supported. And users can customize the merge rules on the background page{link}.", + "lookTitle": "What's rules look like?", + "p3": "The rule consists of two parts, the source part and the target part. The source part declares which sites hit the rule, and the target part defines how those sites are merged. For example, {demo} is a common rule, the left is the source site, and the right is the target site.", + "sourceCol": "Source part", + "targetCol": "Target part", + "remarkCol": "Remark", + "source": { + "title": "How to define the source part?", + "p1": "The source part can be a specific domain name or an Ant expression. Below are some examples.", + "exampleCol": "Examples of matched site", + "only": "Only {source} can hit this rule" + }, + "target": { + "title": "How to define the target part?", + "p1": "The target part can be a specific domain name, a positive integer, or be left blank. They will be introduced one by one in conjunction with the source part in the table below.", + "lookCol": "Look", + "remark": { + "blank": "{source} won't be merged cause of blank target part", + "spec": "Sites hitting {source} will be merged into the specific {target}", + "integer": "Sites hitting {source} will be merged into the last {target} level domain names", + "specFirst": "When multiple rules are hit, the source part takes precedence for a specific domain name", + "miss": "Merge to the before level of Public Suffix List{link} when no rules are hit" + }, + "p2": "The following table is some merging examples after the above rules are set at the same time.", + "originalCol": "Original site", + "mergedCol": "Merged site", + "hitCol": "Hitted rule" + } + }, + "zh_CN": { + "title": "合并统计相关站点", + "p1": "这个扩展是按照域名进行统计的,比方说 {demo1} 和 {demo2} 会被统计到 2 个条目。如果您想要查看这两个网站的数据汇总的话,就需要使用合并功能。", + "p2": "在大多数的数据展示界面,都支持站点合并查询。并且用户可以在后台页自定义合并规则{link}。", + "lookTitle": "规则长什么样?", + "p3": "规则由两部分组成,源和目标。源部分声明哪些站点会命中该规则,而目标部分定义如何合并这些站点。比如 {demo} 就是一个常见的规则,左边的是源,右边的是目标。", + "source": { + "title": "如何定义源部分?", + "p1": "源部分可以是具体的域名,也可以是 Ant 表达式。下面是一些例子。", + "exampleCol": "匹配网站的示例", + "only": "只有 {source} 能够命中该规则" + }, + "sourceCol": "源部分", + "targetCol": "目标部分", + "remarkCol": "备注", + "target": { + "title": "如何定义目标部分?", + "p1": "目标部分可以是具体的域名,正整数,或者留空。将在下表中结合源部分一一介绍。", + "lookCol": "规则外观", + "remark": { + "blank": "{source} 不会被合并,因为目标部分为空", + "spec": "满足 {source} 的网站会被合并到指定条目 {target}", + "integer": "满足 {source} 的网站,在合并时会保留后 {target} 级域名", + "specFirst": "命中多个规则时,源部分是具体域名的优先", + "miss": "没有命中任何规则时,合并至 Public Suffix List{link} 的前一级" + }, + "p2": "下表是上述规则同时设置后的一些合并示例。", + "originalCol": "原始站点", + "mergedCol": "合并后站点", + "hitCol": "命中的规则" + } + }, + "zh_TW": { + "title": "合併統計相關站點", + "p1": "這個擴展是按照域名進行統計的,比方說 {demo1} 和 {demo2} 會被統計到 2 個條目。如果您想要查看這兩個網站的數據匯總的話,就需要使用合併功能。", + "p2": "在大多數的數據展示界面,都支持站點合併查詢。並且用戶可以在後台頁自定義合併規則{link}。", + "lookTitle": "規則長什麼樣?", + "p3": "規則由兩部分組成,源和目標。源部分聲明哪些站點會命中該規則,而目標部分定義如何合併這些站點。比如 {demo} 就是一個常見的規則,左邊的是源,右邊的是目標。", + "source": { + "title": "如何定義源部分?", + "p1": "源部分可以是具體的域名,也可以是 Ant 表達式。下面是一些例子。", + "exampleCol": "匹配網站的示例", + "only": "只有 {source} 能够命中该规则" + }, + "sourceCol": "源部分", + "targetCol": "目標部分", + "remarkCol": "備註", + "target": { + "title": "如何定義目標部分?", + "p1": "目標部分可以是具體的域名,正整數,或者留空。將在下表中結合源部分一一介紹。", + "lookCol": "規則外觀", + "remark": { + "blank": "{source} 不会被合并,因为目标部分为空", + "spec": "滿足 {source} 的網站會被合併到指定條目 {target}", + "integer": "滿足 {source} 的網站,在合併時會保留後 {target} 級域名", + "specFirst": "命中多個規則時,源部分是具體域名的優先", + "miss": "沒有命中任何規則時,合併至 Public Suffix List{link} 的前一級" + }, + "p2": "下表是上述規則同時設置後的一些合併示例。", + "originalCol": "原始站點", + "mergedCol": "合併後站點", + "hitCol": "命中的規則" + } + }, + "ja": { + "title": "関連サイトのデータをまとめます", + "p1": "この拡張子はドメイン名でカウントされます。たとえば、{demo1} と {demo2} は 2 つのレコードとしてカウントされます。両方のサイトの集計データを表示する場合は、マージ機能を使用する必要があります。", + "p2": "ほとんどのデータ表示ページでは、マージされたクエリがサポートされています。 また、ユーザーはバックグラウンド ページ {link} でマージ ルールをカスタマイズできます。", + "lookTitle": "ルールはどのように見えますか?", + "p3": "ルールは、ソース部分とターゲット部分の 2 つの部分で構成されます。ソース部分はルールに一致するサイトを宣言し、ターゲット部分はそれらのサイトがどのようにマージされるかを定義します。たとえば、{demo} は一般的なルールで、左側がソース サイト、右側がターゲット サイトです。", + "sourceCol": "ソース部分", + "targetCol": "対象部位", + "remarkCol": "述べる", + "source": { + "title": "ソース パーツを定義する方法", + "p1": "ソース部分は、特定のドメイン名または Ant 式にすることができます。 以下にいくつかの例を示します。", + "exampleCol": "マッチしたサイトの例", + "only": "このルールに該当するのは {source} だけです" + }, + "target": { + "title": "ターゲット パーツを定義する方法", + "p1": "ターゲット部分は、特定のドメイン名、正の整数、または空白のままにすることができます。下表のソース部分と合わせて順次紹介していきます。", + "lookCol": "外観", + "remark": { + "blank": "{source} はマージされません 空白のターゲット パーツが原因です", + "spec": "{source} にヒットしたサイトは、特定の {target} にマージされます", + "integer": "{source} にヒットしたサイトは、最後の {target} レベルのドメイン名にマージされます", + "specFirst": "複数のルールがヒットした場合、特定のドメイン名についてソース部分が優先されます", + "miss": "ルールにヒットしない場合は、Public Suffix List{link} の前のレベルにマージします" + }, + "p2": "次の表は、上記のルールを同時に設定した後のいくつかのマージ例です。", + "originalCol": "元のサイト", + "mergedCol": "統合サイト", + "hitCol": "ヒットルール" + } + }, + "pt_PT": { + "title": "Resumir dados de sites relacionados", + "p1": "Esta extensão é contada pelo nome de domínio, por exemplo, {demo1} e {demo2} serão contados como 2 registos. Se quer ver os dados agregados para ambos os sites, precisará usar o recurso de mesclagem.", + "p2": "Na maioria das páginas de exibição de dados, são suportadas consultas mescladas. E os utilizadores podem personalizar as regras de mesclagem na página de segundo plano{link}.", + "lookTitle": "Quais são as regras?", + "p3": "A regra consiste em duas partes, a parte de origem e a parte de destino. A parte de origem declara quais sites atingem a regra, e a parte de destino define como esses sites são mesclados. Por exemplo, {demo} é uma regra comum, a esquerda é o site de origem, e a direita é o site alvo.", + "sourceCol": "Parte fonte", + "targetCol": "Parte alvo", + "remarkCol": "Observação", + "source": { + "title": "Como definir a parte de origem?", + "p1": "A parte de origem pode ser um nome de domínio específico ou uma expressão Ant. Abaixo estão alguns exemplos.", + "exampleCol": "Exemplos de sites correspondentes", + "only": "Somente {source} pode atingir essa regra" + }, + "target": { + "title": "Como definir a parte alvo?", + "p1": "A parte de destino pode ser um domínio específico, um inteiro positivo ou ficar em branco. Elas serão introduzidas uma a uma, em conjunto com a parte de origem na tabela abaixo.", + "lookCol": "Aparência", + "remark": { + "blank": "{source} não será mesclada causa de destino em branco", + "spec": "Sítios web que acertarem {source} serão mesclados no {target} específico", + "integer": "Sítios web que atingirem {source} serão mesclados aos últimos {target} nomes de domínio", + "specFirst": "Quando múltiplas regras são atingidas, a parte de origem tem prioridade para um nome de domínio específico", + "miss": "Mesclar ao nível anterior da lista de sufixos públicos{link} quando nenhuma regra for atingida" + }, + "p2": "A tabela a seguir é alguns exemplos de mesclagem depois que as regras acima são definidas em simultâneo.", + "originalCol": "Site original", + "mergedCol": "Site mesclado", + "hitCol": "Regra negada" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/merge.ts b/src/i18n/message/guide/merge.ts new file mode 100644 index 000000000..f5dbc8752 --- /dev/null +++ b/src/i18n/message/guide/merge.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import resource from './merge-resource.json' + +export type MergeMessage = { + title: string + p1: string + p2: string + lookTitle: string + p3: string + sourceCol: string + targetCol: string + remarkCol: string + source: { + title: string + p1: string + exampleCol: string + only: string + } + target: { + title: string + p1: string + lookCol: string + remark: { + blank: string + spec: string + integer: string + specFirst: string + miss: string + } + p2: string + originalCol: string + mergedCol: string + hitCol: string + } +} + +const _default: Messages = resource + +export default _default diff --git a/src/i18n/message/guide/privacy-resource.json b/src/i18n/message/guide/privacy-resource.json new file mode 100644 index 000000000..4526d6add --- /dev/null +++ b/src/i18n/message/guide/privacy-resource.json @@ -0,0 +1,172 @@ +{ + "zh_CN": { + "title": "隐私声明", + "alert": "为了向您提供完整的服务,该扩展在使用过程中会必要地收集您的一些个人数据,详情见以下隐私声明。", + "scope": { + "title": "哪些数据会被收集?", + "cols": { + "name": "内容", + "usage": "用途", + "required": "是否必需" + }, + "rows": { + "website": { + "name": "网站访问记录", + "usage": "用于统计浏览时长和访问次数" + }, + "tab": { + "name": "浏览器标签信息", + "usage": "用于自动获取网站的名称和图标,展示数据时提升用户体验", + "optionalReason": "只有在选项里开启自动获取功能后才会收集" + }, + "clipboard": { + "name": "剪切板内容", + "usage": "在设置每日时限规则时,为了操作方便,会读取剪切板内 URL", + "optionalReason": "需要用户手动同意" + } + } + }, + "storage": { + "title": "如何处理这些数据?", + "p1": "我们保证该扩展收集的所有数据只会保存在您的浏览器本地存储中,绝不会将他们分发到其他地方。", + "p2": "不过您可以使用扩展提供的工具,以 JSON 或者 CSV 的文件格式,导出或者导入您的数据。扩展也支持您使用 GitHub Gist 等,您足以信任的第三方服务,备份您的数据。", + "p3": "我们只帮助您收集数据,但处置权一定在您。" + } + }, + "zh_TW": { + "title": "隱私聲明", + "alert": "為了向您提供完整的服務,該擴展在使用過程中會必要地收集您的一些個人數據,詳情見以下隱私聲明。", + "scope": { + "title": "哪些數據會被收集?", + "cols": { + "name": "內容", + "usage": "用途", + "required": "是否必需" + }, + "rows": { + "website": { + "name": "網站訪問記錄", + "usage": "用於統計瀏覽時長和訪問次數" + }, + "tab": { + "name": "瀏覽器標籤信息", + "usage": "用於自動獲取網站的名稱和圖標,展示數據時提升用戶體驗", + "optionalReason": "只有在選項裡開啟自動獲取功能後才會收集" + }, + "clipboard": { + "name": "剪切板內容", + "usage": "在設置每日時限規則時,為了操作方便,會讀取剪切板內 URL", + "optionalReason": "需要用戶手動同意" + } + } + }, + "storage": { + "title": "如何處理這些數據?", + "p1": "我們保證該擴展收集的所有數據只會保存在您的瀏覽器本地存儲中,絕不會將他們分發到其他地方。", + "p2": "不過您可以使用擴展提供的工具,以 JSON 或者 CSV 的文件格式,導出或者導入您的數據。擴展也支持您使用 GitHub Gist 等,您足以信任的第三方服務,備份您的數據。", + "p3": "我們只幫助您收集數據,但處置權一定在您。" + } + }, + "en": { + "title": "Privacy statement", + "alert": "In order to provide you with complete services, this extension will necessarily collect some of your personal data during use, see the following privacy statement for details.", + "scope": { + "title": "What data is collected?", + "cols": { + "name": "Content", + "usage": "Usage", + "required": "Required" + }, + "rows": { + "website": { + "name": "Website browsing history", + "usage": "Used to count browsing time and visits" + }, + "tab": { + "name": "Tab information", + "usage": "Used to automatically obtain the name and icon of the website, and improve user experience when displaying data", + "optionalReason": "Only if this function is enabled in the options" + }, + "clipboard": { + "name": "Clipboard content", + "usage": "When setting the daily time limit rule, for the convenience of operation, the URL in the clipboard will be read", + "optionalReason": "Only if user agreed" + } + } + }, + "storage": { + "title": "How to do with this data?", + "p1": "We guarantee that all data collected by this extension will only be saved in your browser's local storage and will never be distributed elsewhere.", + "p2": "You can however use the tools provided by the extension to export or import your data in JSON or CSV file format. The extension also supports you to use GitHub Gist, etc., third-party services you trust enough to back up your data.", + "p3": "We only help you collect data, but the right of disposal must be yours." + } + }, + "ja": { + "title": "プライバシーに関する声明", + "alert": "完全なサービスを提供するために、この拡張機能は使用中に必ず個人データの一部を収集します。詳細については、次のプライバシーに関する声明を参照してください。", + "scope": { + "title": "どのようなデータが収集されますか?", + "cols": { + "name": "収集データ", + "usage": "使用", + "required": "それは必要ですか" + }, + "rows": { + "website": { + "name": "ウェブサイトの閲覧履歴", + "usage": "閲覧時間と訪問をカウントするために使用されます" + }, + "tab": { + "name": "タブ情報", + "usage": "Web サイトの名前とアイコンを自動的に取得し、データを表示する際のユーザー エクスペリエンスを向上させるために使用されます", + "optionalReason": "この機能がオプションで有効になっている場合のみ" + }, + "clipboard": { + "name": "クリップボードの内容", + "usage": "毎日の時間制限ルールを設定すると、操作の便宜上、クリップボードの URL が読み込まれます", + "optionalReason": "ユーザーが同意した場合のみ" + } + } + }, + "storage": { + "title": "このデータをどうするか?", + "p1": "この拡張機能によって収集されたすべてのデータは、ブラウザのローカル ストレージにのみ保存され、他の場所に配布されることはありません。", + "p2": "ただし、拡張機能によって提供されるツールを使用して、データを JSON または CSV ファイル形式でエクスポートまたはインポートできます。 この拡張機能は、GitHub Gist など、データをバックアップするのに十分信頼できるサードパーティ サービスの使用もサポートします。", + "p3": "私たちはあなたがデータを収集するのを手伝うだけですが、処分する権利はあなたのものでなければなりません." + } + }, + "pt_PT": { + "title": "Declaração de privacidade", + "alert": "Para fornecer serviços completos, esta extensão irá necessariamente coletar alguns dos seus dados pessoais durante o uso, consulte a seguinte declaração de privacidade para obter detalhes.", + "scope": { + "title": "Quais as informações recolhidas?", + "cols": { + "name": "Conteúdo", + "usage": "Utilização", + "required": "Requeridas" + }, + "rows": { + "website": { + "name": "Histórico de navegação de sites", + "usage": "Usado para contar o tempo de navegação e visitas" + }, + "tab": { + "name": "Informação de guia", + "usage": "Usado para obter automaticamente o nome e o ícone do site, e melhorar a experiência do utilizador ao exibir dados", + "optionalReason": "Somente se esta função estiver ativada nas opções" + }, + "clipboard": { + "name": "Conteúdo da área de transferência", + "usage": "Ao definir a regra de limite de tempo diário, para a conveniência da operação, a URL da área de transferência será lida", + "optionalReason": "Somente se utilizador concordar" + } + } + }, + "storage": { + "title": "Como fazer com esses dados?", + "p1": "Garantimos que todos os dados coletados por esta extensão só serão salvos no armazenamento local do seu navegador e nunca serão distribuídos em outro lugar.", + "p2": "No entanto, pode usar as ferramentas fornecidas pela extensão para exportar ou importar os seus dados em formato JSON, ou CSV. A extensão também oferece suporte a você para usar o GitHub Gist, etc., serviços de terceiros nos quais confia o suficiente para fazer backup dos seus dados.", + "p3": "Só lhe ajudamos a recolher dados, mas o direito à eliminação deve ser o seu." + } + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/privacy.ts b/src/i18n/message/guide/privacy.ts index 7e8401c28..db30f2f5a 100644 --- a/src/i18n/message/guide/privacy.ts +++ b/src/i18n/message/guide/privacy.ts @@ -1,80 +1,51 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import resource from './privacy-resource.json' type _StoreKey = + | 'title' | 'p1' | 'p2' | 'p3' -type _ScopeKey = - | 'p1' - | 'l1' - | 'l2' - | 'l3' + +type RequiredScope = { + name: string + usage: string +} + +type OptionalScope = RequiredScope & { + optionalReason: string +} + +export type Scope = RequiredScope & { + optionalReason?: string +} export type PrivacyMessage = { - scope: { [key in _ScopeKey]: string } + title: string + alert: string + scope: { + title: string + cols: { + name: string + usage: string + required: string + } + rows: { + website: RequiredScope + tab: OptionalScope + clipboard: OptionalScope + } + } storage: { [key in _StoreKey]: string } } -const _default: Messages = { - zh_CN: { - scope: { - p1: '为了向您提供完整的服务,该扩展在使用过程中会收集您以下的个人信息:', - l1: '1. 您浏览网站的时间,以及访问每个网站的次数。', - l2: '2. 网站的标题以及图标 URL。', - l3: '3. 为了提高用户体验,扩展在必要时会征得您的授权之后,读取您的剪切板内容。', - }, - storage: { - p1: '我们保证该扩展收集的所有数据只会保存在您的浏览器本地存储中,绝不会将他们分发到其他地方。', - p2: '不过您可以使用扩展提供的工具,以 JSON 或者 CSV 的文件格式,导出或者导入您的数据。扩展也支持您使用 GitHub Gist 等,您足以信任的第三方服务,备份您的数据。', - p3: '我们只帮助您收集数据,但处置权一定在您。', - }, - }, - zh_TW: { - scope: { - p1: '為了向您提供完整的服務,該擴展在使用過程中會收集您以下的個人信息:', - l1: '1. 您瀏覽網站的時間,以及訪問每個網站的次數。', - l2: '2. 網站的標題以及圖標 URL。', - l3: '3. 為了提高用戶體驗,擴展在必要時會徵得您的授權之後,讀取您的剪切板內容。', - }, - storage: { - p1: '我們保證該擴展收集的所有數據只會保存在您的瀏覽器本地存儲中,絕不會將他們分發到其他地方。', - p2: '不過您可以使用擴展提供的工具,以 JSON 或者 CSV 的文件格式,導出或者導入您的數據。擴展也支持您使用 GitHub Gist 等,您足以信任的第三方服務,備份您的數據。', - p3: '我們只幫助您收集數據,但處置權一定在您。', - }, - }, - en: { - scope: { - p1: 'In order to provide you with complete services, this extension will collect your following personal information during use:', - l1: '1. How long you browse the site, and how many times you visit each site.', - l2: '2. The title of the website and the URL of the icon.', - l3: '3. In order to improve user experience, the extension will read your clipboard content after obtaining your authorization when necessary.', - }, - storage: { - p1: 'We guarantee that all data collected by this extension will only be saved in your browser\'s local storage and will never be distributed elsewhere.', - p2: 'You can however use the tools provided by the extension to export or import your data in JSON or CSV file format. The extension also supports you to use GitHub Gist, etc., third-party services you trust enough to back up your data.', - p3: 'We only help you collect data, but the right of disposal must be yours.', - }, - }, - ja: { - scope: { - p1: '完全なサービスを提供するために、この拡張機能は使用中に次の個人情報を収集します。', - l1: '1. サイトを閲覧した時間と、各サイトにアクセスした回数。', - l2: '2. ウェブサイトのタイトルとアイコンの URL。', - l3: '3. ユーザー エクスペリエンスを向上させるために、拡張機能は必要に応じて承認を得た後、クリップボードの内容を読み取ります。', - }, - storage: { - p1: 'この拡張機能によって収集されたすべてのデータは、ブラウザのローカル ストレージにのみ保存され、他の場所に配布されることはありません。', - p2: 'ただし、拡張機能によって提供されるツールを使用して、データを JSON または CSV ファイル形式でエクスポートまたはインポートできます。 この拡張機能は、GitHub Gist など、データをバックアップするのに十分信頼できるサードパーティ サービスの使用もサポートします。', - p3: '私たちはあなたがデータを収集するのを手伝うだけですが、処分する権利はあなたのものでなければなりません.', - }, - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/profile.ts b/src/i18n/message/guide/profile.ts deleted file mode 100644 index afa6c876a..000000000 --- a/src/i18n/message/guide/profile.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -type _key = - | 'p1' - | 'p2' - -export type ProfileMessage = { - [key in _key]: string -} - -const _default: Messages = { - zh_CN: { - p1: '{appName}是一款开源、免费、用户友好的,用于统计上网时间的浏览器扩展。您可以在 {github} 上查阅它的源代码。', - p2: '这个页面将会告诉您如何使用它,以及相关的隐私政策。', - }, - zh_TW: { - p1: '{appName}是一款開源、免費、用戶友好的,用於統計上網時間的瀏覽器擴展。您可以在 {github} 上查閱它的源代碼。', - p2: '這個頁面將會告訴您如何使用它,以及相關的隱私政策。', - }, - en: { - p1: '{appName} is a browser extension to track the time you spent on all websites.You can check out its source code on {github}.', - p2: 'This page will tell you how to use it, and the related privacy policy.', - }, - ja: { - p1: '{appName}は、オンラインで費やした時間をカウントするための、オープン ソースで無料のユーザー フレンドリーなブラウザ拡張機能です。 {github} でソース コードを確認できます。 ', - p2: 'このページでは、使用方法と関連するプライバシー ポリシーについて説明します。', - }, -} - -export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/start-resource.json b/src/i18n/message/guide/start-resource.json new file mode 100644 index 000000000..ea4eb771a --- /dev/null +++ b/src/i18n/message/guide/start-resource.json @@ -0,0 +1,57 @@ +{ + "zh_CN": { + "title": "快速开始", + "p1": "只需简单三步,就可以快速开始使用这个扩展。", + "s1": "1. 固定扩展的图标", + "s1p1": "首先,为了更方便地使用这个扩展,你需要将图标固定到工具栏上。不同浏览器的操作方式不同,下图显示了在 Chrome 中的做法。", + "s2": "2. 浏览任何网站", + "s2p1": "然后你可以点开任何网站,扩展图标上会展示该网站的当日浏览时间,就像这样 {demo}。", + "s3": "3. 在弹出页中查看数据", + "s3p1": "最后,点击扩展的图标,你可以在弹出的页面里看到今天、本周以及本月的饼状图数据。", + "alert": "你已经学会了基本用法,赶紧试试!!" + }, + "en": { + "title": "Get started", + "p1": "You can quickly start using this extension in just 3 easy steps.", + "s1": "1. Pin the icon", + "s1p1": "Firstly, to use this extension more conveniently, you'd better pin the icon to toolbar. It's not the same in different browsers to do this, the following figure shows how in Chrome.", + "s2": "2. Browse any website", + "s2p1": "Then, browse any website, and you will see that the time is beating on the icon, just like this {demo}.", + "s3": "3. Read data in the popup page", + "s3p1": "Finally, click the icon to open the popup page, and you can read the data visualized with pie chart, of today, this week or this month.", + "alert": "You have learned the basic usage, try it!!" + }, + "zh_TW": { + "title": "快速開始", + "p1": "只需簡單三步,就可以快速開始使用這個擴展。", + "s1": "1. 固定擴展的圖標", + "s1p1": "首先,為了更方便地使用這個擴展,你需要將圖標固定到工具欄上。不同瀏覽器的操作方式不同,下圖顯示了在 Chrome 中的做法。", + "s2": "2. 瀏覽任何網站", + "s2p1": "然後你可以點開任何網站,擴展圖標上會展示該網站的當日瀏覽時間,就像這樣 {demo}。", + "s3": "3. 在彈出頁中查看數據", + "s3p1": "最後,點擊擴展的圖標,你可以在彈出的頁面裡看到今天、本週以及本月的餅狀圖數據。", + "alert": "你已經學會了基本用法,趕緊試試!!" + }, + "ja": { + "title": "始めましょう", + "p1": "この拡張機能は、わずか 3 つの簡単な手順ですぐに使い始めることができます。", + "s1": "1. アイコンを固定する", + "s1p1": "まず、この拡張機能をより便利に使用するには、アイコンをツールバーにピン留めすることをお勧めします。 これを行う方法はブラウザーによって異なります。次の図は、Chrome での方法を示しています。", + "s2": "2. 任意の Web サイトを閲覧する", + "s2p1": "次に、任意の Web サイトを閲覧すると、この {demo} のように、時間がアイコンに刻み込まれていることがわかります。", + "s3": "3. ポップアップ ページでデータを読み取る", + "s3p1": "最後にアイコンをクリックしてポップアップページを開くと、今日、今週、今月のデータを円グラフで可視化して読むことができます。", + "alert": "基本的な使い方を学んだので、試してみてください!!" + }, + "pt_PT": { + "title": "Iniciar", + "p1": "Pode começar rapidamente a usar essa extensão em apenas 3 etapas fáceis.", + "s1": "1. Fixar o ícone", + "s1p1": "Em primeiro lugar, para usar esta extensão mais convenientemente, é melhor fixar o ícone na barra de ferramentas. Não é o mesmo em diferentes navegadores fazer isso, a seguinte figura mostra como no Chrome.", + "s2": "2. Procure qualquer site", + "s2p1": "Em seguida, navegue em qualquer site, e verá que o tempo está batendo no ícone, assim como este {demo}.", + "s3": "3. Ler dados na página pop-up", + "s3p1": "Finalmente, clique no ícone para abrir a página do pop-up, e pode ler os dados visualizados com gráfico de pizza, de hoje, esta semana ou este mês.", + "alert": "Aprendeu o uso básico, experimente!!" + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/start.ts b/src/i18n/message/guide/start.ts new file mode 100644 index 000000000..4e00c6fd8 --- /dev/null +++ b/src/i18n/message/guide/start.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import resource from './start-resource.json' + +type _Key = + | 'p1' + | 's1' + | 's1p1' + | 's2' + | 's2p1' + | 's3' + | 's3p1' + | 'alert' + +export type StartMessage = { + title: string +} & { + [key in _Key]: string + } + +const _default: Messages = resource + +export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/usage.ts b/src/i18n/message/guide/usage.ts deleted file mode 100644 index 2026a441b..000000000 --- a/src/i18n/message/guide/usage.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -type _QuickstartKey = - | 'p1' - | 'l1' - | 'l2' - | 'l3' - | 'p2' - -type _BackgroundKey = - | 'p1' - | 'l1' - | 'l2' - | 'p2' - | 'backgroundPage' - -type _AdvancedKey = - | 'p1' - | 'l1' - | 'l2' - | 'l3' - | 'l4' - | 'l5' - | 'l6' - | 'l7' - | 'l8' - -type _BackupKey = - | 'p1' - | 'l1' - | 'l2' - | 'l3' - -export type UsageMessage = { - quickstart: { [key in _QuickstartKey]: string } - background: { [key in _BackgroundKey]: string } - advanced: { [key in _AdvancedKey]: string } - backup: { [key in _BackupKey]: string } -} - -const _default: Messages = { - zh_CN: { - quickstart: { - p1: '首先,您可以通过以下几步,开始体验这个扩展:', - l1: '1. 将扩展的图标固定在浏览器的右上角,具体的操作方法根据您的浏览器而定。该步骤不会影响扩展的正常运行,但是将极大改善您的交互体验。', - l2: '2. 打开任意一个网站,浏览几秒钟,这时您会观察到右上角的图标上有数字跳动,它显示您今天花了多少时间浏览当前网站。', - l3: '3. 点击扩展的图标,会弹出一个页面,展示今天或最近一段时间您的上网数据。', - p2: '需要提醒的是,由于时长数据是实时统计,所以安装扩展之前的浏览记录不会被记录。', - }, - background: { - p1: '基于图标,扩展提供了比较便捷的数据查看方式。但是如果您想要体验它的全部功能,就需要访问扩展的 {background}。进入后台页有以下两种方式:', - l1: '1. 您可以右击扩展的图标,在弹出的菜单中点击【{allFunction}】。', - l2: '2. 您在图标弹出页的下方也可以找到【{allFunction}】链接,同样点击它即可。', - p2: '弹出页和后台页是这个扩展最主要的交互方式,当你知道如何打开他们之后,就可以完整地使用它了。', - backgroundPage: '后台页', - }, - advanced: { - p1: '这个扩展的核心功能是统计您在不同网站上的浏览行为。此外,它也提供了很多高级功能,来满足您更多的需求。当然,所有的功能您都可以在后台页里找到。', - l1: '1. 它可以分析您在一段时间内访问同一个网站的趋势,并以折线图展示。', - l2: '2. 它可以统计您在每天的不同时间段里的上网频率,并以直方图展示。该数据不区分网站,最小的统计粒度为 15 分钟。', - l3: '3. 它可以统计您阅读本地文件的时间,不过该功能需要在选项中启用。', - l4: '4. 它支持白名单功能,您可以将您不想要统计的网站加入白名单。', - l5: '5. 它支持将几个相关的网站合并统计到同一个条目,您可以自定义合并的规则。默认按照 {psl} 合并。', - l6: '6. 它支持限制每个网站的每日浏览时长,需要您手动添加限制规则。', - l7: '7. 它支持夜间模式,同样需要在选项里启用。', - l8: '8. 它支持使用 Github Gist 作为云端存储多个浏览器的数据,并进行聚合查询。需要您准备一个至少包含 gist 权限的 token。', - }, - backup: { - p1: '您可以按以下步骤使用 {gist} 备份您的数据。之后,您可在其他终端上查询已备份数据。', - l1: '1. 首先,您需要在 Github 生成一个包含 gist 权限的 {token}。', - l2: '2. 然后在选项页面将同步方式选为 Github Gist,将你的 token 填入下方出现的输入框中。', - l3: '3. 最后,点击备份按钮即可将本地数据导入到你的 gist 里。', - }, - }, - zh_TW: { - quickstart: { - p1: '首先,您可以通過以下幾步,開始體驗這個擴展:', - l1: '1. 將擴展的圖標固定在瀏覽器的右上角,具體的操作方法根據您的瀏覽器而定。該步驟不會影響擴展的正常運行,但是將極大改善您的交互體驗。', - l2: '2. 打開任意一個網站,瀏覽幾秒鐘,這時您會觀察到右上角的圖標上有數字跳動,它顯示您今天花了多少時間瀏覽當前網站。', - l3: '3. 點擊擴展的圖標,會彈出一個頁面,展示今天或最近一段時間您的上網數據。', - p2: '需要提醒的是,由於時長數據是實時統計,所以安裝擴展之前的瀏覽記錄不會被記錄。', - }, - background: { - p1: '基於圖標,擴展提供了比較便捷的數據查看方式。但是如果您想要體驗它的全部功能,就需要訪問擴展的 {background}。進入後台頁有以下兩種方式:', - l1: '1. 您可以右擊擴展的圖標,在彈出的菜單中點擊【{allFunction}】。', - l2: '2. 您在圖標彈出頁的下方也可以找到【{allFunction}】鏈接,同樣點擊它即可。', - p2: '彈出頁和後台頁是這個擴展最主要的交互方式,當你知道如何打開他們之後,就可以完整地使用它了。', - backgroundPage: '後台頁', - }, - advanced: { - p1: '這個擴展的核心功能是統計您在不同網站上的瀏覽行為。此外,它也提供了很多高級功能,來滿足您更多的需求。當然,所有的功能您都可以在後台頁裡找到。', - l1: '1. 它可以分析您在一段時間內訪問同一個網站的趨勢,並以折線圖展示。', - l2: '2. 它可以統計您在每天的不同時間段裡的上網頻率,並以直方圖展示。該數據不區分網站,最小的統計粒度為 15 分鐘。', - l3: '3. 它可以統計您閱讀本地文件的時間,不過該功能需要在選項中啟用。', - l4: '4. 它支持白名單功能,您可以將您不想要統計的網站加入白名單。', - l5: '5. 它支持將幾個相關的網站合併統計到同一個條目,您可以自定義合併的規則。默認按照 {psl} 合併。', - l6: '6. 它支持限制每個網站的每日瀏覽時長,需要您手動添加限制規則。', - l7: '7. 它支持夜間模式,同樣需要在選項裡啟用。', - l8: '8. 它支持使用 Github Gist 作為雲端存儲多個瀏覽器的數據,並進行聚合查詢。需要您準備一個至少包含 gist 權限的 token。', - }, - backup: { - p1: '您可以按以下步驟使用 {gist} 備份您的數據。之後,您可在其他終端上查詢已備份數據。', - l1: '1. 首先,您需要在 Github 生成一個包含 gist 權限的 {token}。', - l2: '2. 然後在選項頁面將同步方式選為 Github Gist,將你的 token 填入下方出現的輸入框中。', - l3: '3. 最後,點擊備份按鈕即可將本地數據導入到你的 gist 裡。', - }, - }, - en: { - quickstart: { - p1: 'First, you can quickly start using this extension by following these steps:', - l1: '1. Pin the icon of this extension in the upper right corner of the browser. The specific operation method depends on your browser.This step will not affect the normal behavior of it, but will greatly improve your interactive experience.', - l2: '2. Visit any website and browse for a few seconds, then you will observe a number jumping on the icon.it shows how much time you spent today browsing current website', - l3: '3. Click the icon, and a page will pop up, showing your stat data for today or recent days.', - p2: 'It is worth mentioning that since the duration data can only be counted in real time,the history before installation will not be recorded.', - }, - background: { - p1: 'Based on icons, the extension provides a more convenient way to view data.But if you want to experience its full functionality, you need to visit {background} of the extension.There are two ways to enter the background page:', - l1: '1. You can right-click the icon of the extension, and click [{allFunction}] in the pop-up menu.', - l2: '2. You can also find the [{allFunction}] link at the bottom of the icon popup page, just click it.', - p2: 'The popup page and background page are the main interaction methods of this extension. After you know how to open them, you can use it completely.', - backgroundPage: 'the background page', - }, - advanced: { - p1: 'The core function of this extension is to count your browsing behavior on different websites.In addition, it also provides many advanced functions to meet your more needs.Of course, you can find all the functions in the background page.', - l1: '1. It can analyze the trend of your visiting the same website over a period of time, and display it in a line chart.', - l2: '2. It can count your surfing frequency in different time periods every day, and display it in a histogram.The data is site-agnostic and has a minimum statistical granularity of 15 minutes.', - l3: '3. It can count the time you read local files, but this function needs to be enabled in the options.', - l4: '4. It supports the whitelist function, you can add the websites you don\'t want to count to the whitelist.', - l5: '5. It supports merging statistics of several related websites into the same entry, and you can customize the rules for merging. Merge by {psl} by default.', - l6: '6. It supports limiting the daily browsing time of each website, which requires you to manually add limiting rules.', - l7: '7. It supports night mode, which also needs to be enabled in the options.', - l8: '8. It supports using Github Gist as the cloud to store data of multiple browsers and perform aggregated queries. You need to prepare a token with at least gist permission.', - }, - backup: { - p1: 'You can use {gist} to backup your data by following the steps below.Afterwards, you can query the backed up data on other terminals.', - l1: '1. First, you need to generate a {token} with gist permissions on Github.', - l2: '2. Then select Github Gist as the synchronization method on the options page,and fill in your token in the input box that appears below.', - l3: '3. Finally, click the backup button to import the local data into your gist.', - }, - }, - ja: { - quickstart: { - p1: 'まず、次の手順に従って、この拡張機能の調査を開始できます。', - l1: '1. ブラウザの右上隅にある拡張機能のアイコンを修正します。具体的な操作方法はブラウザによって異なります。 この手順は、拡張機能の通常の操作には影響しませんが、インタラクティブなエクスペリエンスを大幅に向上させます。', - l2: '2. 任意の Web サイトを開いて数秒間ブラウジングすると、右上隅のアイコンに数字がジャンプしていることがわかります。これは、現在の Web サイトの閲覧に今日どれだけの時間を費やしたかを示しています。', - l3: '3. 拡張機能のアイコンをクリックすると、ページがポップアップし、今日または最近のインターネット データが表示されます。', - p2: 'なお、継続時間データはリアルタイムでカウントされるため、拡張機能をインストールする前の閲覧履歴は記録されません。', - }, - background: { - p1: 'アイコンに基づいて、拡張機能はデータを表示するためのより便利な方法を提供します。 ただし、完全な機能を体験したい場合は、拡張 {background} にアクセスする必要があります。 バックグラウンド ページに入る方法は 2 つあります。', - l1: '1. 拡張機能のアイコンを右クリックし、ポップアップ メニューで [{allFunction}] をクリックします。', - l2: '2. また、アイコン ポップアップ ページの下部に [{allFunction}] リンクがあり、それをクリックするだけです。', - p2: 'ポップアップ ページと背景ページは、この拡張機能の主な対話方法であり、それらを開く方法を理解すれば、完全に使用できます。', - backgroundPage: '背景ページ', - }, - advanced: { - p1: 'この拡張機能の主な機能は、さまざまな Web サイトでの閲覧行動をカウントすることです。 さらに、より多くのニーズを満たすために多くの高度な機能も提供します。 もちろん、バックグラウンド ページですべての機能を見つけることができます。', - l1: '1. 一定期間の同じ Web サイトへのアクセスの傾向を分析し、折れ線グラフで表示できます。', - l2: '2. あなたのネットサーフィン頻度を毎日異なる時間帯でカウントし、ヒストグラムで表示できます。 データはサイトにとらわれず、最小の統計粒度は 15 分です。', - l3: '3. ローカル ファイルの読み取り時間をカウントできますが、この機能はオプションで有効にする必要があります。', - l4: '4. ホワイトリスト機能をサポートしており、カウントしたくない Web サイトをホワイトリストに追加できます。', - l5: '5. 複数の関連 Web サイトの統計を同じエントリにマージすることをサポートし、マージのルールをカスタマイズできます。 デフォルトでは {psl} でマージします。', - l6: '6. 各 Web サイトの毎日の閲覧時間の制限をサポートしています。これには、制限ルールを手動で追加する必要があります。', - l7: '7.オプションで有効にする必要があるナイトモードをサポートしています。', - l8: '8. Github Gist をクラウドとして使用して、複数のブラウザーのデータを保存し、集約されたクエリを実行することをサポートします。 少なくとも gist 権限を持つトークンを準備する必要があります。', - }, - backup: { - p1: '以下の手順に従って、{gist} を使用してデータをバックアップできます。その後、バックアップされたデータを他の端末で照会できます。', - l1: '1. まず、Github で Gist 権限を持つ {token} を生成する必要があります。', - l2: '2. 次に、オプション ページで同期方法として [Github Gist] を選択し、下に表示される入力ボックスにトークンを入力します。', - l3: '3. 最後に、バックアップ ボタンをクリックして、ローカル データを Gist にインポートします。', - }, - }, -} - -export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/virtual-resource.json b/src/i18n/message/guide/virtual-resource.json new file mode 100644 index 000000000..c038be51c --- /dev/null +++ b/src/i18n/message/guide/virtual-resource.json @@ -0,0 +1,57 @@ +{ + "en": { + "title": "Count data for specific URLs", + "p1": "If you want to count the browsing time and number of visits of certain URLs, not just the domain name, you can do so by creating a virtual site.", + "step": { + "title": "Four steps to create a virtual site", + "enter": "First, enter the management page{link}, click the menu item {menuItem}.", + "click": "Click the New button in the upper right corner.", + "form": "Then, fill in the URL to count, Ant expressions is allowed, such as {demo1} or {demo2}, and the name of this virtual site, and then click Save.", + "browse": "Finally, browse the relevant URLs and observe the data." + } + }, + "zh_CN": { + "title": "统计特定 URL 的数据", + "p1": "如果你想统计某些 URL ,而不只是域名的浏览时长和访问次数,可以通过创建虚拟站点来实现。", + "step": { + "title": "简单四步创建一个虚拟站点", + "enter": "首先进入管理页{link},点击菜单项【{menuItem}】。", + "click": "然后单击右上角的新建按钮。", + "form": "填写要统计的 URL,可以使用 Ant 表达式,如 {demo1} 或 {demo2} ,以及这个虚拟站点的名称,然后点击保存。", + "browse": "最后浏览相关的网页,观察数据。" + } + }, + "zh_TW": { + "title": "統計特定 URL 的數據", + "p1": "如果你想統計某些 URL ,而不只是域名的瀏覽時長和訪問次數,可以通過創建虛擬站點來實現。", + "step": { + "title": "簡單四步創建一個虛擬站點", + "enter": "首先進入管理頁{link},點擊菜單項【{menuItem}】。", + "click": "然後單擊右上角的新建按鈕。", + "form": "填寫要統計的 URL,可以使用 Ant 表達式,如 {demo1} 或 {demo2} ,以及這個虛擬站點的名稱,然後點擊保存。", + "browse": "最後瀏覽相關的網頁,觀察數據。" + } + }, + "ja": { + "title": "特定の URL のデータをカウントする", + "p1": "ドメイン名だけでなく、特定の URL の閲覧時間と訪問回数をカウントしたい場合は、仮想サイトを作成することで実行できます。", + "step": { + "title": "仮想サイトを作成するための 4 つのステップ", + "enter": "まず、管理ページ {link} に入り、メニュー項目 {menuItem} をクリックします。", + "click": "右上隅にある [新規] ボタンをクリックします。", + "form": "次に、カウントする URL、{demo1} または {demo2} などの Ant 式が許可されていること、およびこの仮想サイトの名前を入力し、[保存] をクリックします。", + "browse": "最後に、関連する URL を参照してデータを観察します。" + } + }, + "pt_PT": { + "title": "Contar dados para URL específicos", + "p1": "Se deseja contar o tempo de navegação e o número de visitas de certas URL, não apenas o nome de domínio, pode fazê-lo criando um site virtual.", + "step": { + "title": "Quatro etapas para criar um site virtual", + "enter": "Primeiro, digite a página de gestão{link}, clique no item de menu {menuItem}.", + "click": "Clique no botão Criar no canto superior direito.", + "form": "Em seguida, preencha a URL para contar, expressões Ant são permitidas, como {demo1} ou {demo2}, o nome deste site virtual e clique em Salvar.", + "browse": "Finalmente, procure os URL relevantes e observe os dados." + } + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/virtual.ts b/src/i18n/message/guide/virtual.ts new file mode 100644 index 000000000..5fab3fe08 --- /dev/null +++ b/src/i18n/message/guide/virtual.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import resource from './virtual-resource.json' + +export type VirtualMessage = { + title: string + p1: string + step: { + title: string + enter: string + click: string + form: string + browse: string + } +} + +const _default: Messages = resource + +export default _default diff --git a/src/i18n/message/popup/chart-resource.json b/src/i18n/message/popup/chart-resource.json new file mode 100644 index 000000000..eafa8ec88 --- /dev/null +++ b/src/i18n/message/popup/chart-resource.json @@ -0,0 +1,107 @@ +{ + "zh_CN": { + "title": { + "today": "今日数据", + "thisWeek": "本周数据", + "thisMonth": "本月数据", + "last30Days": "近 30 天数据" + }, + "mergeHostLabel": "合并子域名", + "fileName": "上网时长清单_{today}_by_{app}", + "saveAsImageTitle": "保存", + "restoreTitle": "刷新", + "options": "设置", + "totalTime": "共 {totalTime}", + "totalCount": "共 {totalCount} 次", + "averageTime": "平均每天 {value}", + "averageCount": "平均每天 {value} 次", + "otherLabel": "其他{count}个网站", + "updateVersion": "版本升级", + "updateVersionInfo": "最新版本:{version}", + "updateVersionInfo4Firefox": "新版本 {version} 已发布\n\n您可以前往插件管理页进行更新" + }, + "zh_TW": { + "title": { + "today": "今日數據", + "thisWeek": "本週數據", + "thisMonth": "本月數據", + "last30Days": "近 30 天數據" + }, + "mergeHostLabel": "合並子網域", + "fileName": "上網時長清單_{today}_by_{app}", + "saveAsImageTitle": "保存", + "restoreTitle": "刷新", + "options": "設置", + "totalTime": "共 {totalTime}", + "totalCount": "共 {totalCount} 次", + "averageCount": "平均每天 {value} 次", + "averageTime": "平均每天 {value}", + "otherLabel": "其他 {count} 個站點", + "updateVersion": "版本昇級", + "updateVersionInfo": "最新版本:{version}", + "updateVersionInfo4Firefox": "新版本 {version} 已髮佈\n\n您可以前往插件管理頁進行更新" + }, + "en": { + "title": { + "today": "Today's Data", + "thisWeek": "This Week's Data", + "thisMonth": "This Month's Data", + "last30Days": "Last 30 days' data" + }, + "mergeHostLabel": "Merge Sites", + "fileName": "Web_Time_List_{today}_By_{app}", + "saveAsImageTitle": "Snapshot", + "restoreTitle": "Restore", + "options": "Options", + "totalTime": "Total {totalTime}", + "totalCount": "Total {totalCount} times", + "averageCount": "{value} times per day on average", + "averageTime": "{value} per day on average", + "otherLabel": "Other {count} sites", + "updateVersion": "Updatable", + "updateVersionInfo": "Latest: {version}", + "updateVersionInfo4Firefox": "Upgrade to {version} in the management page, about:addons, pls" + }, + "ja": { + "title": { + "today": "今日のデータ", + "thisWeek": "今週のデータ", + "thisMonth": "今月のデータ", + "last30Days": "過去 30 日間のデータ" + }, + "mergeHostLabel": "URLをマージ", + "fileName": "オンライン時間_{today}_by_{app}", + "saveAsImageTitle": "ダウンロード", + "restoreTitle": "刷新", + "options": "設定", + "totalTime": "合計 {totalTime}", + "totalCount": "合計 {totalCount} 回", + "averageTime": "1日平均 {value}", + "averageCount": "1日平均 {value} 回", + "otherLabel": "他 {count} サイト", + "updateVersion": "更新", + "updateVersionInfo": "最新バージョン:{version}", + "updateVersionInfo4Firefox": "管理ページで {version} にアップグレードしてください" + }, + "pt_PT": { + "mergeHostLabel": "Mesclar Sítios Web", + "fileName": "Lista_de_Horários_da_Web_{today}_por_{app}", + "saveAsImageTitle": "Capturas", + "restoreTitle": "Restaurar", + "options": "Opções", + "totalTime": "Total {totalTime}", + "totalCount": "Total {totalCount} vezes", + "averageCount": "{value} vezes por dia em média", + "averageTime": "{value} por dia em média", + "otherLabel": "Outros sites do {count}", + "updateVersion": "Atualizável", + "updateVersionInfo": "Último: {version}", + "updateVersionInfo4Firefox": "Atualize para {version} na página de gestão, about:addons, por favor", + "title": { + "today": "Dados de Hoje", + "thisWeek": "Dados desta Semana", + "thisMonth": "Dados deste Mês", + "last30Days": "Dados dos últimos 30 dias" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/popup/chart.ts b/src/i18n/message/popup/chart.ts index ec117a37d..de52ec824 100644 --- a/src/i18n/message/popup/chart.ts +++ b/src/i18n/message/popup/chart.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './chart-resource.json' + export type ChartMessage = { title: { [key in PopupDuration]: string } mergeHostLabel: string @@ -22,92 +24,7 @@ export type ChartMessage = { updateVersionInfo4Firefox: string } -const _default: Messages = { - zh_CN: { - title: { - today: '今日数据', - thisWeek: '本周数据', - thisMonth: '本月数据', - last30Days: '近 30 天数据', - }, - mergeHostLabel: '合并子域名', - fileName: '上网时长清单_{today}_by_{app}', - saveAsImageTitle: '保存', - restoreTitle: '刷新', - options: '设置', - totalTime: '共 {totalTime}', - totalCount: '共 {totalCount} 次', - averageTime: '平均每天 {value}', - averageCount: '平均每天 {value} 次', - otherLabel: '其他{count}个网站', - updateVersion: '版本升级', - updateVersionInfo: '最新版本:{version}', - updateVersionInfo4Firefox: '新版本 {version} 已发布\n\n您可以前往插件管理页进行更新', - }, - zh_TW: { - title: { - today: '今日數據', - thisWeek: '本週數據', - thisMonth: '本月數據', - last30Days: '近 30 天數據', - }, - mergeHostLabel: '合並子網域', - fileName: '上網時長清單_{today}_by_{app}', - saveAsImageTitle: '保存', - restoreTitle: '刷新', - options: '設置', - totalTime: '共 {totalTime}', - totalCount: '共 {totalCount} 次', - averageCount: '平均每天 {value} 次', - averageTime: '平均每天 {value}', - otherLabel: '其他{count}個站點', - updateVersion: '版本昇級', - updateVersionInfo: '最新版本:{version}', - updateVersionInfo4Firefox: '新版本 {version} 已髮佈\n\n您可以前往插件管理頁進行更新', - }, - en: { - title: { - today: 'Today\'s Data', - thisWeek: 'This Week\'s Data', - thisMonth: 'This Month\'s Data', - last30Days: 'Last 30 days\' data', - }, - mergeHostLabel: 'Merge Sites', - fileName: 'Web_Time_List_{today}_By_{app}', - saveAsImageTitle: 'Snapshot', - restoreTitle: 'Restore', - options: 'Options', - totalTime: 'Total {totalTime}', - totalCount: 'Total {totalCount} times', - averageCount: '{value} times per day on average', - averageTime: '{value} per day on average', - otherLabel: 'Other {count} sites', - updateVersion: 'Updatable', - updateVersionInfo: 'Latest: {version}', - updateVersionInfo4Firefox: 'Upgrade to {version} in the management page, about:addons, pls', - }, - ja: { - title: { - today: '今日のデータ', - thisWeek: '今週のデータ', - thisMonth: '今月のデータ', - last30Days: '過去 30 日間のデータ', - }, - mergeHostLabel: 'URLをマージ', - fileName: 'オンライン時間_{today}_by_{app}', - saveAsImageTitle: 'ダウンロード', - restoreTitle: '刷新', - options: '設定', - totalTime: '合計 {totalTime}', - totalCount: '合計 {totalCount} 回', - averageTime: '1日平均 {value}', - averageCount: '1日平均 {value} 回', - otherLabel: '他{count}サイト', - updateVersion: '更新', - updateVersionInfo: '最新バージョン:{version}', - updateVersionInfo4Firefox: '管理ページで {version} にアップグレードしてください', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/popup/index.ts b/src/i18n/message/popup/index.ts index 47cefa183..aff4391e6 100644 --- a/src/i18n/message/popup/index.ts +++ b/src/i18n/message/popup/index.ts @@ -48,6 +48,13 @@ const _default: Messages = { meta: metaMessages.ja, base: baseMessages.ja, }, + pt_PT: { + chart: chartMessages.pt_PT, + duration: popupDurationMessages.pt_PT, + item: itemMessages.pt_PT, + meta: metaMessages.pt_PT, + base: baseMessages.pt_PT, + }, } export default _default \ No newline at end of file diff --git a/src/manifest.ts b/src/manifest.ts index 5a47abfd0..d9591bab9 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -14,7 +14,7 @@ // Not use path alias in manifest.json import packageInfo from "./package" import { OPTION_ROUTE } from "./app/router/constants" -const { version, author, homepage } = packageInfo +const { version, author: { name: author }, homepage } = packageInfo const _default: chrome.runtime.ManifestV2 = { name: '__MSG_meta_marketName__', description: "__MSG_meta_description__", diff --git a/src/package.ts b/src/package.ts index c442574d7..20fcd31c5 100644 --- a/src/package.ts +++ b/src/package.ts @@ -19,4 +19,9 @@ const _default: _PackageInfo = { author: packageJson.author } +/** + * @since 1.8.0 + */ +export const AUTHOR_EMAIL: string = packageJson.author.email + export default _default \ No newline at end of file diff --git a/src/service/components/host-merge-ruler.ts b/src/service/components/host-merge-ruler.ts index 70e7ad199..d4a3d30a3 100644 --- a/src/service/components/host-merge-ruler.ts +++ b/src/service/components/host-merge-ruler.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -37,9 +37,12 @@ type RegRuleItem = { result: string | number } -const processRegStr = (regStr: string) => regStr.split('.').join('\\.').split('*').join('[^\\.]+') +const processRegStr = (regStr: string) => regStr + .split('.').join('\\.') + .split('**').join('.+') + .split('*').join('[^\\.]+') -const convert = (dbItem: timer.merge.Rule) => { +function convert(dbItem: timer.merge.Rule): RegRuleItem | [string, string | number] { const { origin, merged } = dbItem if (origin.includes('*')) { const regStr = processRegStr(origin) @@ -57,7 +60,9 @@ export default class CustomizedHostMergeRuler implements timer.merge.Merger { constructor(rules: timer.merge.Rule[]) { rules.map(item => convert(item)) - .forEach(rule => Array.isArray(rule) ? (this.noRegMergeRules[rule[0]] = rule[1]) : (this.regulars.push(rule))) + .forEach(rule => Array.isArray(rule) + ? (this.noRegMergeRules[rule[0]] = rule[1] || rule[0]) + : (this.regulars.push(rule))) } /** diff --git a/src/service/limit-service/index.ts b/src/service/limit-service/index.ts new file mode 100644 index 000000000..af0312629 --- /dev/null +++ b/src/service/limit-service/index.ts @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { listTabs, sendMsg2Tab } from "@api/chrome/tab" +import { DATE_FORMAT } from "@db/common/constant" +import LimitDatabase from "@db/limit-database" +import TimeLimitItem from "@entity/time-limit-item" +import { formatTime } from "@util/time" +import whitelistHolder from '../components/whitelist-holder' + +const storage = chrome.storage.local +const db: LimitDatabase = new LimitDatabase(storage) + +export type QueryParam = { + filterDisabled: boolean + url: string +} + +async function select(cond?: QueryParam): Promise { + const { filterDisabled, url } = cond ? cond : { filterDisabled: undefined, url: undefined } + const today = formatTime(new Date(), DATE_FORMAT) + return (await db.all()) + .filter(item => filterDisabled ? item.enabled : true) + .map(({ cond, time, enabled, wasteTime, latestDate, allowDelay }) => TimeLimitItem.builder() + .cond(cond) + .time(time) + .enabled(enabled) + .waste(latestDate === today ? wasteTime : 0) + .allowDelay(allowDelay) + .build() + ) + // If use url, then test it + .filter(item => url ? item.matches(url) : true) +} + +/** + * Fired if the item is removed or disabled + * + * @param item + */ +async function handleLimitChanged() { + const allItems: TimeLimitItem[] = await select({ filterDisabled: false, url: undefined }) + const tabs = await listTabs() + tabs.forEach(tab => { + const limitedItems = allItems.filter(item => item.matches(tab.url) && item.enabled && item.hasLimited()) + sendMsg2Tab(tab?.id, 'limitChanged', limitedItems) + .catch(err => console.log(err.message)) + }) +} + +async function updateEnabled(item: timer.limit.Item): Promise { + const { cond, time, enabled, allowDelay } = item + const limit: timer.limit.Rule = { cond, time, enabled, allowDelay } + await db.save(limit, true) + await handleLimitChanged() +} + +async function updateDelay(item: timer.limit.Item) { + await db.updateDelay(item.cond, item.allowDelay) + await handleLimitChanged() +} + +async function remove(item: timer.limit.Item): Promise { + await db.remove(item.cond) + await handleLimitChanged() +} + +async function getLimited(url: string): Promise { + const list: TimeLimitItem[] = (await select()) + .filter(item => item.enabled) + .filter(item => item.matches(url)) + .filter(item => item.hasLimited()) + return list +} + +/** + * Add time + * @param url url + * @param focusTime time, milliseconds + * @returns the rules is limit cause of this operation + */ +async function addFocusTime(url: string, focusTime: number) { + const allEnabled: TimeLimitItem[] = await select({ filterDisabled: true, url }) + const toUpdate: { [cond: string]: number } = {} + const result: TimeLimitItem[] = [] + allEnabled.forEach(item => { + const limitBefore = item.hasLimited() + toUpdate[item.cond] = item.waste += focusTime + const limitAfter = item.hasLimited() + if (!limitBefore && limitAfter) { + result.push(item) + } + }) + await db.updateWaste(formatTime(new Date, DATE_FORMAT), toUpdate) + return result +} + +async function moreMinutes(url: string, rules?: TimeLimitItem[]): Promise { + if (rules === undefined || rules === null) { + rules = (await select({ url: url, filterDisabled: true })) + .filter(item => item.hasLimited() && item.allowDelay) + } + const date = formatTime(new Date(), DATE_FORMAT) + const toUpdate: { [cond: string]: number } = {} + rules.forEach(rule => { + const { cond, waste } = rule + const updatedWaste = (waste || 0) - 5 * 60 * 1000 + rule.waste = toUpdate[cond] = updatedWaste < 0 ? 0 : updatedWaste + }) + await db.updateWaste(date, toUpdate) + return rules +} + +class LimitService { + moreMinutes = moreMinutes + getLimited = getLimited + updateEnabled = updateEnabled + updateDelay = updateDelay + select = select + remove = remove + /** + * @returns The rules limited cause of this operation + */ + async addFocusTime(host: string, url: string, focusTime: number): Promise { + if (whitelistHolder.notContains(host)) { + return addFocusTime(url, focusTime) + } else { + return [] + } + } +} + +export default new LimitService() diff --git a/src/service/limit-service/verification/common.ts b/src/service/limit-service/verification/common.ts new file mode 100644 index 000000000..fbc8d02c2 --- /dev/null +++ b/src/service/limit-service/verification/common.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { I18nKey } from "@i18n" +import { LimitMessage } from "@i18n/message/app/limit" + +type LimitVerificationMessage = LimitMessage['verification'] + +export type VerificationContext = { + difficulty: timer.limit.VerificationDifficulty + locale: timer.Locale +} + +export type VerificationPair = { + prompt?: I18nKey | string + promptParam?: any + answer: I18nKey | string +} + +/** + * Verification code generator + */ +export interface VerificationGenerator { + /** + * Whether to support + */ + supports(context: VerificationContext): boolean + + /** + * Render the prompt + */ + generate(context: VerificationContext): VerificationPair +} \ No newline at end of file diff --git a/src/service/limit-service/verification/generator/confession.ts b/src/service/limit-service/verification/generator/confession.ts new file mode 100644 index 000000000..be4414a91 --- /dev/null +++ b/src/service/limit-service/verification/generator/confession.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { VerificationContext, VerificationGenerator, VerificationPair } from "../common" + +/** + * Generator of confession + */ +class ConfessionGenerator implements VerificationGenerator { + supports(context: VerificationContext): boolean { + return context.difficulty === 'easy' + } + generate(_: VerificationContext): VerificationPair { + return { + answer: msg => msg.confession, + } + } +} + +export default ConfessionGenerator \ No newline at end of file diff --git a/src/service/limit-service/verification/generator/index.ts b/src/service/limit-service/verification/generator/index.ts new file mode 100644 index 000000000..49f2ed8ec --- /dev/null +++ b/src/service/limit-service/verification/generator/index.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ +import { VerificationGenerator } from "../common" + +import ConfessionGenerator from "./confession" +import PiGenerator from "./pi" +import UglyGenerator from "./ugly" +import UncommonChinese from "./uncommon-chinese" + +export const ALL_GENERATORS: VerificationGenerator[] = [ + new PiGenerator(), + new ConfessionGenerator(), + new UglyGenerator(), + new UncommonChinese(), +] \ No newline at end of file diff --git a/src/service/limit-service/verification/generator/pi.ts b/src/service/limit-service/verification/generator/pi.ts new file mode 100644 index 000000000..16c5dc9ca --- /dev/null +++ b/src/service/limit-service/verification/generator/pi.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { randomIntBetween } from "@util/number" +import { VerificationContext, VerificationGenerator, VerificationPair } from "../common" + +const MIN_START_IDX = 10 +const MAX_START_IDX = 25 +const MIN_LEN = 5 +const MAX_LEN = 15 + +const DIGIT_PART_PI = '14159265358979323846264338327950288419716939937510' + + '58209749445923078164062862089986280348253421170679' + +/** + * Generator of pi + */ +class PiGenerator implements VerificationGenerator { + generate(_: VerificationContext): VerificationPair { + const startIndex = randomIntBetween(MIN_START_IDX, MAX_START_IDX) + const digitCount = randomIntBetween(MIN_LEN, MAX_LEN) + const endIndex = startIndex + digitCount - 1 + const answer = DIGIT_PART_PI.substring(startIndex - 1, endIndex) + return { + answer, + prompt: msg => msg.pi, + promptParam: { + startIndex, + endIndex, + digitCount, + } + } + } + + supports(context: VerificationContext): boolean { + return context.difficulty === 'disgusting' + } +} + +export default PiGenerator \ No newline at end of file diff --git a/src/service/limit-service/verification/generator/ugly.ts b/src/service/limit-service/verification/generator/ugly.ts new file mode 100644 index 000000000..46cdcb21d --- /dev/null +++ b/src/service/limit-service/verification/generator/ugly.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { randomIntBetween } from "@util/number" +import { VerificationContext, VerificationGenerator, VerificationPair } from "../common" + +class UglyGenerator implements VerificationGenerator { + supports(context: VerificationContext): boolean { + return context.difficulty === 'hard' + } + + generate(_: VerificationContext): VerificationPair { + const min = 1 << 6 + const max = 1 << 8 + const random = randomIntBetween(min, max) + return { + answer: random?.toString(2) + } + } +} + +export default UglyGenerator \ No newline at end of file diff --git a/src/service/limit-service/verification/generator/uncommon-chinese.ts b/src/service/limit-service/verification/generator/uncommon-chinese.ts new file mode 100644 index 000000000..c3d42e6a0 --- /dev/null +++ b/src/service/limit-service/verification/generator/uncommon-chinese.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { randomIntBetween } from "@util/number" +import { VerificationContext, VerificationGenerator, VerificationPair } from "../common" + +const UNCOMMON_WORDS = '龘靐齉齾爩鱻麤龗灪吁龖厵滟爨癵籱饢驫鲡鹂鸾麣纞虋讟钃骊郁鸜麷鞻韽韾响顟顠饙饙騳騱饐' +const LENGTH = UNCOMMON_WORDS.length + +class UncommonChinese implements VerificationGenerator { + supports(context: VerificationContext): boolean { + return context.difficulty === 'disgusting' && context.locale === 'zh_CN' + } + + generate(_: VerificationContext): VerificationPair { + let answer = '' + while (answer.length < 3) { + const idx = randomIntBetween(0, LENGTH) + const ch = UNCOMMON_WORDS[idx] + if (!answer.includes(ch)) { + answer += ch + } + } + return { + answer + } + } +} + +export default UncommonChinese \ No newline at end of file diff --git a/src/service/limit-service/verification/processor.ts b/src/service/limit-service/verification/processor.ts new file mode 100644 index 000000000..860261b57 --- /dev/null +++ b/src/service/limit-service/verification/processor.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { VerificationContext, VerificationGenerator, VerificationPair } from "./common" +import { ALL_GENERATORS } from "./generator" + +class VerificationProcessor { + generators: VerificationGenerator[] + + constructor() { + this.generators = ALL_GENERATORS + } + + generate(difficulty: timer.limit.VerificationDifficulty, locale: timer.Locale): VerificationPair { + const context: VerificationContext = { difficulty, locale } + const supported = this.generators.filter(g => g.supports(context)) + const len = supported?.length + if (!len) { + return null + } + let generator = supported[0] + if (len > 1) { + const idx = Math.floor(Math.random() * supported.length) + generator = supported[idx] + } + return generator.generate(context) + } +} + +export default new VerificationProcessor() \ No newline at end of file diff --git a/src/service/option-service.ts b/src/service/option-service.ts index 067d908cf..911693d0f 100644 --- a/src/service/option-service.ts +++ b/src/service/option-service.ts @@ -11,6 +11,7 @@ import { defaultPopup, defaultStatistics, defaultBackup, + defaultDailyLimit, } from "@util/constant/option" const db = new OptionDatabase(chrome.storage.local) @@ -19,6 +20,7 @@ const defaultOption = () => ({ ...defaultAppearance(), ...defaultPopup(), ...defaultStatistics(), + ...defaultDailyLimit(), ...defaultBackup(), }) @@ -41,6 +43,11 @@ async function setStatisticsOption(option: timer.option.StatisticsOption): Promi await setOption(option) } +async function setDailyLimitOption(option: timer.option.DailyLimitOption): Promise { + // Rewrite password + await setOption(option) +} + async function setBackupOption(option: Partial): Promise { // Rewrite auths const existOption = await getAllOption() @@ -92,6 +99,10 @@ class OptionService { setPopupOption = setPopupOption setAppearanceOption = setAppearanceOption setStatisticsOption = setStatisticsOption + /** + * @since 1.9.0 + */ + setDailyLimitOption = setDailyLimitOption /** * @since 1.2.0 */ diff --git a/src/service/site-service.ts b/src/service/site-service.ts index aad393fb8..3df9a05bd 100644 --- a/src/service/site-service.ts +++ b/src/service/site-service.ts @@ -6,10 +6,12 @@ */ import SiteDatabase, { SiteCondition } from "@db/site-database" +import StatDatabase from "@db/stat-database" import { slicePageResult } from "./components/page-info" const storage = chrome.storage.local const siteDatabase = new SiteDatabase(storage) +const statDatabase = new StatDatabase(storage) export type SiteQueryParam = SiteCondition @@ -63,6 +65,10 @@ class SiteService { return result } + selectAll(param?: SiteQueryParam): Promise { + return siteDatabase.select(param) + } + async batchSelect(keys: timer.site.SiteKey[]): Promise { return siteDatabase.getBatch(keys) } diff --git a/src/util/constant/option.ts b/src/util/constant/option.ts index ba4d7b178..5265b81d9 100644 --- a/src/util/constant/option.ts +++ b/src/util/constant/option.ts @@ -34,7 +34,6 @@ export function defaultAppearance(): timer.option.AppearanceOption { darkModeTimeStart: 64800, // 6*60*60 darkModeTimeEnd: 21600, - limitMarkFilter: 'translucent', } } @@ -46,6 +45,15 @@ export function defaultStatistics(): timer.option.StatisticsOption { } } +export function defaultDailyLimit(): timer.option.DailyLimitOption { + return { + limitLevel: 'nothing', + limitFilter: 'translucent', + limitPassword: '', + limitVerifyDifficulty: 'easy', + } +} + export function defaultBackup(): timer.option.BackupOption { return { backupType: 'none', @@ -62,5 +70,6 @@ export function defaultOption(): timer.option.AllOption { ...defaultAppearance(), ...defaultStatistics(), ...defaultBackup(), + ...defaultDailyLimit(), } } \ No newline at end of file diff --git a/src/util/constant/url.ts b/src/util/constant/url.ts index d4e5aaaac..300753ab0 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -8,7 +8,7 @@ import { getRuntimeId } from "@api/chrome/runtime" import { IS_FIREFOX, IS_CHROME, IS_EDGE } from "./environment" -export const FIREFOX_HOMEPAGE = 'https://addons.mozilla.org/zh-CN/firefox/addon/web%E6%99%82%E9%96%93%E7%B5%B1%E8%A8%88/' +export const FIREFOX_HOMEPAGE = 'https://addons.mozilla.org/firefox/addon/besttimetracker' export const CHROME_HOMEPAGE = 'https://chrome.google.com/webstore/detail/%E7%BD%91%E8%B4%B9%E5%BE%88%E8%B4%B5-%E4%B8%8A%E7%BD%91%E6%97%B6%E9%97%B4%E7%BB%9F%E8%AE%A1/dkdhhcbjijekmneelocdllcldcpmekmm' export const EDGE_HOMEPAGE = 'https://microsoftedge.microsoft.com/addons/detail/timer-the-web-time-is-e/fepjgblalcnepokjblgbgmapmlkgfahc' @@ -66,7 +66,8 @@ export const UNINSTALL_QUESTIONNAIRE: { [locale in timer.Locale]: string } = { zh_CN: 'https://www.wjx.cn/vj/YDgY9Yz.aspx', zh_TW: 'https://docs.google.com/forms/d/e/1FAIpQLSdK93q-548dK-2naoS3DaArdc7tEGoUY9JQvaXP5Kpov8h6-A/viewform?usp=sf_link', ja: 'https://docs.google.com/forms/d/e/1FAIpQLSdsB3onZuleNf6j7KJJLbcote647WV6yeUr-9m7Db5QXakfpg/viewform?usp=sf_link', - en: 'https://docs.google.com/forms/d/e/1FAIpQLSflhZAFTw1rTUjAEwgxqCaBuhLBBthwEK9fIjvmwWfITLSK9A/viewform?usp=sf_link' + en: 'https://docs.google.com/forms/d/e/1FAIpQLSflhZAFTw1rTUjAEwgxqCaBuhLBBthwEK9fIjvmwWfITLSK9A/viewform?usp=sf_link', + pt_PT: 'https://docs.google.com/forms/d/e/1FAIpQLSflhZAFTw1rTUjAEwgxqCaBuhLBBthwEK9fIjvmwWfITLSK9A/viewform?usp=sf_link', } /** @@ -90,8 +91,8 @@ export const UPDATE_PAGE = updatePage */ export function getAppPageUrl(isInBackground: boolean, route?: string, query?: any): string { let url = IS_FIREFOX && !isInBackground ? 'app.html' : 'static/app.html' - const queries = query ? Object.entries(query).map(([k, v]) => `${k}=${v}`).join('&') : '' route && (url += '#' + route) + const queries = query ? Object.entries(query).map(([k, v]) => `${k}=${v}`).join('&') : '' queries && (url += '?' + queries) return url } @@ -101,8 +102,10 @@ export function getAppPageUrl(isInBackground: boolean, route?: string, query?: a * @param isInBackground invoke in background environment * @since 1.3.2 */ -export function getGuidePageUrl(isInBackground: boolean): string { - return IS_FIREFOX && !isInBackground ? 'guide.html' : 'static/guide.html' +export function getGuidePageUrl(isInBackground: boolean, route?: string): string { + let url = IS_FIREFOX && !isInBackground ? 'guide.html' : 'static/guide.html' + route && (url += '#' + route) + return url } /** @@ -133,4 +136,4 @@ export const CROWDIN_PROJECT_ID = 516822 * * @since 1.4.0 */ -export const CROWDIN_HOMEPAGE = 'https://crowdin.com/project/timer-chrome-edge-firefox' \ No newline at end of file +export const CROWDIN_HOMEPAGE = 'https://crowdin.com/project/timer-chrome-edge-firefox' diff --git a/src/util/dark-mode.ts b/src/util/dark-mode.ts index 0b0679305..d3615739d 100644 --- a/src/util/dark-mode.ts +++ b/src/util/dark-mode.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -25,7 +25,9 @@ function toggle0(isDarkMode: boolean) { * Init from local storage */ export function init() { - toggle0(isDarkMode()) + const val = isDarkMode() + toggle0(val) + return val } export function toggle(isDarkMode: boolean) { diff --git a/src/util/merge.ts b/src/util/merge.ts new file mode 100644 index 000000000..a7d3b8a17 --- /dev/null +++ b/src/util/merge.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { MergeCommonMessage } from "@i18n/message/common/merge" + +export type MergeTagType = '' | 'info' | 'success' + +export function computeMergeType(mergedVal: number | string): MergeTagType { + return typeof mergedVal === 'number' ? 'success' : mergedVal === '' ? 'info' : '' +} + +type TypeFinder = (mergeCommon: MergeCommonMessage | EmbeddedPartial) => string + +export function computeMergeTxt( + originVal: string, + mergedVal: number | string, + i18n: (finder: TypeFinder, param?: any) => string +): string { + const mergeTxt = typeof mergedVal === 'number' + ? i18n(msg => msg?.tagResult?.level, { level: mergedVal + 1 }) + : mergedVal === '' ? i18n(msg => msg?.tagResult?.blank) : mergedVal + return `${originVal} >>> ${mergeTxt}` +} diff --git a/src/util/number.ts b/src/util/number.ts index ca5d02f3e..d8d678888 100644 --- a/src/util/number.ts +++ b/src/util/number.ts @@ -23,3 +23,10 @@ export function tryParseInteger(str: string): [boolean, number | string] { const isInteger: boolean = !isNaN(num) && num.toString().length === str.length return [isInteger, isInteger ? num : str] } + +/** + * Generate random integer between {@param lowerInclusive} and {@param upperExclusive} + */ +export function randomIntBetween(lowerInclusive: number, upperExclusive: number): number { + return Math.floor(Math.random() * (upperExclusive - lowerInclusive)) + lowerInclusive +} \ No newline at end of file diff --git a/src/util/time.ts b/src/util/time.ts index 9954cbe47..4e6edacef 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -59,6 +59,8 @@ export function formatTime(time: Date | string | number, cFormat?: string) { * Format milliseconds for display */ export function formatPeriod(milliseconds: number, message: { hourMsg: string, minuteMsg: string, secondMsg: string }): string { + const prefix = milliseconds < 0 ? '-' : '' + milliseconds = Math.abs(milliseconds) const { hourMsg, minuteMsg, secondMsg } = message const seconds = Math.floor(milliseconds / 1000) const hour = Math.floor(seconds / 3600) @@ -67,7 +69,10 @@ export function formatPeriod(milliseconds: number, message: { hourMsg: string, m let msg = hourMsg hour === 0 && (msg = minuteMsg) && minute === 0 && (msg = secondMsg) - return msg.replace('{hour}', hour.toString()).replace('{minute}', minute.toString()).replace('{second}', second.toString()) + const result = msg.replace('{hour}', hour.toString()) + .replace('{minute}', minute.toString()) + .replace('{second}', second.toString()) + return prefix + result } /** @@ -233,4 +238,4 @@ export function getAllDatesBetween(dateStart: Date, dateEnd: Date): string[] { } while (cursor.getTime() < dateEnd.getTime()) isSameDay(cursor, dateEnd) && dates.push(formatTime(dateEnd, format)) return dates -} \ No newline at end of file +} diff --git a/types/timer/index.d.ts b/types/timer/index.d.ts index 1809c54bd..63caea4c2 100644 --- a/types/timer/index.d.ts +++ b/types/timer/index.d.ts @@ -15,6 +15,8 @@ declare namespace timer { | 'ja' // @since 0.9.0 | 'zh_TW' + // @since 1.8.2 + | 'pt_PT' /** * @since 0.8.0 */ @@ -30,8 +32,6 @@ declare namespace timer { | 'es' | 'ko' | 'pl' - | 'pt' - | 'pt_BR' | 'ru' | 'uk' | 'fr' diff --git a/types/timer/limit.d.ts b/types/timer/limit.d.ts index 3d5b79d2e..bf4f5027e 100644 --- a/types/timer/limit.d.ts +++ b/types/timer/limit.d.ts @@ -44,4 +44,24 @@ declare namespace timer.limit { | 'translucent' // ground glass filter | 'groundGlass' + /** + * @since 1.9.0 + */ + type RestrictionLevel = + // No additional action required to lock + | 'nothing' + // Password required to lock or modify restricted rule + | 'password' + // Verification code input requird to lock or modify restricted rule + | 'verification' + /** + * @since 1.9.0 + */ + type VerificationDifficulty = + // Easy + | 'easy' + // Need some operations + | 'hard' + // Disgusting + | 'disgusting' } diff --git a/types/timer/option.d.ts b/types/timer/option.d.ts index 5e78e9552..7330ca9b6 100644 --- a/types/timer/option.d.ts +++ b/types/timer/option.d.ts @@ -103,8 +103,9 @@ declare namespace timer.option { /** * The filter of limit mark * @since 1.3.2 + * @deprecated moved to DailyLimitOption @since 1.9.0 */ - limitMarkFilter: limit.FilterType + limitMarkFilter?: limit.FilterType } type StatisticsOption = { @@ -125,6 +126,25 @@ declare namespace timer.option { countLocalFiles: boolean } + type DailyLimitOption = { + /** + * restriction level + */ + limitLevel: limit.RestrictionLevel + /** + * The filter of limit mark + */ + limitFilter: limit.FilterType + /** + * The password to unlock + */ + limitPassword: string + /** + * The difficulty of verification + */ + limitVerifyDifficulty: limit.VerificationDifficulty + } + /** * The options of backup * @@ -153,7 +173,7 @@ declare namespace timer.option { autoBackUpInterval: number } - type AllOption = PopupOption & AppearanceOption & StatisticsOption & BackupOption + type AllOption = PopupOption & AppearanceOption & StatisticsOption & DailyLimitOption & BackupOption /** * @since 0.8.0 */ diff --git a/webpack/webpack.common.ts b/webpack/webpack.common.ts index 2dc5af0c0..6eb360103 100644 --- a/webpack/webpack.common.ts +++ b/webpack/webpack.common.ts @@ -1,12 +1,14 @@ import path from "path" import GenerateJsonPlugin from "generate-json-webpack-plugin" import CopyWebpackPlugin from "copy-webpack-plugin" -import webpack from "webpack" +import webpack, { Chunk } from "webpack" // Generate json files import manifest from "../src/manifest" import i18nChrome from "../src/i18n/chrome" import tsConfig from '../tsconfig.json' import MiniCssExtractPlugin from "mini-css-extract-plugin" +import HtmlWebpackPlugin from "html-webpack-plugin" + const tsPathAlias = tsConfig.compilerOptions.paths const generateJsonPlugins = [ @@ -41,16 +43,40 @@ Object.entries(tsPathAlias).forEach(([alias, sourceArr]) => { console.log("Alias of typescript: ") console.log(resolveAlias) +type EntryConfig = { + name: string + path: string + chunkExclusive?: boolean +} + +const entryConfigs: EntryConfig[] = [{ + name: 'background', + path: './src/background', + chunkExclusive: true, +}, { + name: 'content_scripts', + path: './src/content-script', + chunkExclusive: true, +}, { + name: 'popup', + path: './src/popup', +}, { + name: 'app', + path: './src/app', +}, { + name: 'guide', + path: './src/guide', +}] + +const EXCLUDE_CHUNK_ENTRY = entryConfigs.filter(({ chunkExclusive }) => chunkExclusive).map(({ name }) => name) + +const excludeChunk = (chunk: Chunk) => !EXCLUDE_CHUNK_ENTRY.includes(chunk.name) + const staticOptions: webpack.Configuration = { - entry: { - background: './src/background', - content_scripts: './src/content-script', - // The entrance of popup page - popup: './src/popup', - // The entrance of app page - app: './src/app', - // The entrance of guide page - guide: './src/guide', + entry() { + const entry = {} + entryConfigs.forEach(({ name, path }) => entry[name] = path) + return entry }, output: { filename: '[name].js', @@ -91,7 +117,49 @@ const staticOptions: webpack.Configuration = { assert: false, // fallbacks of axios's dependencies end } - } + }, + optimization: { + splitChunks: { + cacheGroups: { + echarts: { + name: 'echarts', + chunks: excludeChunk, + test: /[\\/]node_modules[\\/](echarts|zrender)[\\/]/, + }, + // @vue & vue-router + vue: { + name: 'vue', + chunks: excludeChunk, + test: /[\\/]node_modules[\\/]@?vue(use)?(-router)?[\\/]/, + }, + element: { + name: 'element', + chunks: excludeChunk, + test: /[\\/]node_modules[\\/]@?element-plus[\\/]/, + }, + psl: { + name: 'psl', + chunks: excludeChunk, + test: /[\\/]node_modules[\\/]psl[\\/]/, + }, + lodash: { + name: 'lodash', + chunks: excludeChunk, + test: /[\\/]node_modules[\\/]lodash[\\/]/, + }, + axios: { + name: 'axios', + chunks: excludeChunk, + test: /[\\/]node_modules[\\/]axios[\\/]/, + }, + common: { + name: 'common', + chunks: excludeChunk, + test: /[\\/]src[\\/](service|database|util)[\\/]/, + }, + } + }, + }, } const optionGenerator = (outputPath: string, manifestHooker?: (manifest: chrome.runtime.ManifestV2) => void) => { @@ -101,10 +169,28 @@ const optionGenerator = (outputPath: string, manifestHooker?: (manifest: chrome. // copy static resources new CopyWebpackPlugin({ patterns: [ - { from: path.join(__dirname, '..', 'public'), to: path.join(outputPath, 'static') } + { + from: path.join(__dirname, '..', 'public', 'images'), + to: path.join(outputPath, 'static', 'images'), + } ] }), new MiniCssExtractPlugin(), + new HtmlWebpackPlugin({ + template: path.join(__dirname, '..', 'public', 'app.html'), + filename: path.join('static', 'app.html'), + chunks: ['app'], + }), + new HtmlWebpackPlugin({ + template: path.join(__dirname, '..', 'public', 'guide.html'), + filename: path.join('static', 'guide.html'), + chunks: ['guide'], + }), + new HtmlWebpackPlugin({ + template: path.join(__dirname, '..', 'public', 'popup.html'), + filename: path.join('static', 'popup.html'), + chunks: ['popup'], + }), ] return { ...staticOptions,