mirror of
https://github.com/tcsenpai/pensieve.git
synced 2025-06-06 03:05:25 +00:00
feat: i18n support
This commit is contained in:
parent
0eb217aa24
commit
37fefef3ea
9
web/src/i18n.ts
Normal file
9
web/src/i18n.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { init, register } from 'svelte-i18n';
|
||||
|
||||
register('en', () => import('./locales/en.json'));
|
||||
register('zh', () => import('./locales/zh.json'));
|
||||
|
||||
init({
|
||||
fallbackLocale: 'en',
|
||||
initialLocale: 'zh', // 或者根据用户的浏览器设置动态选择
|
||||
});
|
36
web/src/lib/LanguageSwitcher.svelte
Normal file
36
web/src/lib/LanguageSwitcher.svelte
Normal file
@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { locale, _ } from 'svelte-i18n';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
|
||||
let selectedLocale: string;
|
||||
|
||||
const languages = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'zh', label: '中文' }
|
||||
];
|
||||
|
||||
onMount(() => {
|
||||
// 尝试从 localStorage 获取保存的语言
|
||||
const savedLocale = localStorage.getItem('selectedLocale');
|
||||
if (savedLocale) {
|
||||
setLocale(savedLocale);
|
||||
} else {
|
||||
// 如果没有保存的语言,则使用浏览器语言
|
||||
const browserLang = navigator.language.split('-')[0];
|
||||
setLocale(browserLang === 'zh' ? 'zh' : 'en');
|
||||
}
|
||||
});
|
||||
|
||||
function setLocale(newLocale: string) {
|
||||
selectedLocale = newLocale;
|
||||
locale.set(newLocale);
|
||||
localStorage.setItem('selectedLocale', newLocale);
|
||||
}
|
||||
</script>
|
||||
|
||||
<select bind:value={selectedLocale} on:change={() => setLocale(selectedLocale)} class="bg-white text-black">
|
||||
{#each languages as language}
|
||||
<option value={language.value}>{language.label}</option>
|
||||
{/each}
|
||||
</select>
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n'; // 导入翻译函数
|
||||
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
|
||||
@ -33,7 +34,7 @@
|
||||
let selectedLibraries: Record<number, boolean> = {};
|
||||
let allSelected = true;
|
||||
|
||||
let displayName = '全部';
|
||||
let displayName = $_('libraryFilter.all');
|
||||
|
||||
let prevSelectedLibraryIds: number[] = [];
|
||||
|
||||
@ -61,12 +62,12 @@
|
||||
.map((id) => libraries.find(lib => lib.id === id)?.name)
|
||||
.join(', ');
|
||||
} else if (selectedCount === 0) {
|
||||
displayName = '全部';
|
||||
displayName = $_('libraryFilter.all');
|
||||
allSelected = true;
|
||||
selectedLibraries = {};
|
||||
newSelectedLibraryIds = [];
|
||||
} else {
|
||||
displayName = '全部';
|
||||
displayName = $_('libraryFilter.all');
|
||||
allSelected = true;
|
||||
selectedLibraries = {};
|
||||
newSelectedLibraryIds = [];
|
||||
@ -98,13 +99,13 @@
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-56 mt-1 p-1" align="start" side="bottom">
|
||||
<div class="px-2 py-1.5 text-sm font-semibold">
|
||||
<Label class="text-sm font-semibold">仓库筛选</Label>
|
||||
<Label class="text-sm font-semibold">{$_('libraryFilter.repositoryFilter')}</Label>
|
||||
</div>
|
||||
<Separator class="my-1" />
|
||||
<div class="px-2 py-1.5">
|
||||
<div class="mb-2 items-top flex space-x-2">
|
||||
<Checkbox id="all-selected" bind:checked={allSelected} disabled={allSelected} onCheckedChange={toggleSelectAll} />
|
||||
<Label for="all-selected" class="flex items-center text-sm">全选</Label>
|
||||
<Checkbox id="all-selected" bind:checked={allSelected} disabled={allSelected} onCheckedChange={toggleSelectAll} />
|
||||
<Label for="all-selected" class="flex items-center text-sm">{$_('libraryFilter.selectAll')}</Label>
|
||||
</div>
|
||||
{#each libraries as library}
|
||||
<div class="mb-2 items-top flex space-x-2">
|
||||
@ -118,4 +119,4 @@
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,49 +2,56 @@
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { RangeCalendar } from '$lib/components/ui/range-calendar/index.js';
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
||||
import {
|
||||
CalendarDate,
|
||||
DateFormatter,
|
||||
type DateValue,
|
||||
getLocalTimeZone
|
||||
} from '@internationalized/date';
|
||||
|
||||
let now = +new Date();
|
||||
|
||||
const rangeMap = {
|
||||
let rangeMap: {
|
||||
[key: string]: {
|
||||
label: string;
|
||||
start: number | null;
|
||||
end: number | null;
|
||||
};
|
||||
};
|
||||
|
||||
$: rangeMap = {
|
||||
unlimited: {
|
||||
label: '不限时间',
|
||||
label: $_('timeFilter.unlimited'),
|
||||
start: null,
|
||||
end: null
|
||||
},
|
||||
threeHours: {
|
||||
label: '最近3小时',
|
||||
label: $_('timeFilter.threeHours'),
|
||||
start: now - 3 * 60 * 60 * 1000,
|
||||
end: now
|
||||
},
|
||||
today: {
|
||||
label: '今天',
|
||||
label: $_('timeFilter.today'),
|
||||
start: now - 24 * 60 * 60 * 1000,
|
||||
end: now
|
||||
},
|
||||
week: {
|
||||
label: '最近一周',
|
||||
label: $_('timeFilter.week'),
|
||||
start: now - 7 * 24 * 60 * 60 * 1000,
|
||||
end: now
|
||||
},
|
||||
month: {
|
||||
label: '最近一个月',
|
||||
label: $_('timeFilter.month'),
|
||||
start: now - 30 * 24 * 60 * 60 * 1000,
|
||||
end: now
|
||||
},
|
||||
threeMonths: {
|
||||
label: '最近三个月',
|
||||
label: $_('timeFilter.threeMonths'),
|
||||
start: now - 90 * 24 * 60 * 60 * 1000,
|
||||
end: now
|
||||
},
|
||||
custom: {
|
||||
label: '自定义',
|
||||
label: $_('timeFilter.custom'),
|
||||
start: null,
|
||||
end: null
|
||||
}
|
||||
@ -80,7 +87,7 @@
|
||||
|
||||
$: displayText =
|
||||
(timeFilter === 'custom' && customDateRange.start && customDateRange.end)
|
||||
? `${customDateRange.start?.toString()} - ${customDateRange.end?.toString()}`
|
||||
? $_('timeFilter.customRange', { values: { start: customDateRange.start?.toString(), end: customDateRange.end?.toString() } })
|
||||
: rangeMap[timeFilter].label;
|
||||
$: start = rangeMap[timeFilter].start;
|
||||
$: end = rangeMap[timeFilter].end;
|
||||
@ -99,17 +106,17 @@
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="w-56" align="start" side="bottom">
|
||||
<DropdownMenu.Label>时间筛选</DropdownMenu.Label>
|
||||
<DropdownMenu.Label>{$_('timeFilter.label')}</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.RadioGroup bind:value={timeFilter}>
|
||||
<DropdownMenu.RadioItem value="unlimited">时间不限</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="threeHours">最近三小时</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="today">今天</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="week">最近一周</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="month">最近一个月</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="threeMonths">最近三个月</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="unlimited">{$_('timeFilter.unlimited')}</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="threeHours">{$_('timeFilter.threeHours')}</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="today">{$_('timeFilter.today')}</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="week">{$_('timeFilter.week')}</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="month">{$_('timeFilter.month')}</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="threeMonths">{$_('timeFilter.threeMonths')}</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>自定义</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubTrigger>{$_('timeFilter.custom')}</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<RangeCalendar bind:value={customDateRange} />
|
||||
</DropdownMenu.SubContent>
|
||||
@ -117,4 +124,4 @@
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
29
web/src/locales/en.json
Normal file
29
web/src/locales/en.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"libraryFilter": {
|
||||
"all": "All",
|
||||
"repositoryFilter": "Repository Filter",
|
||||
"selectAll": "Select All"
|
||||
},
|
||||
"language": "Language",
|
||||
"timeFilter": {
|
||||
"label": "Time Filter",
|
||||
"unlimited": "No time limit",
|
||||
"threeHours": "Last 3 hours",
|
||||
"today": "Today",
|
||||
"week": "Last week",
|
||||
"month": "Last month",
|
||||
"threeMonths": "Last 3 months",
|
||||
"custom": "Custom",
|
||||
"customRange": "{start} - {end}"
|
||||
},
|
||||
"searchPlaceholder": "Input keyword to search or press Enter to show latest records",
|
||||
"loading": "Loading...",
|
||||
"searchSummary": "✨ {found} results found - Searched {outOf} recipes in {time}ms.",
|
||||
"noResults": "No results found.",
|
||||
"copyright": "© 2024 Arkohut Qinini. All rights reserved.",
|
||||
"privacyPolicy": "Privacy policy",
|
||||
"changelog": "Changelog",
|
||||
"searchLog": {
|
||||
"handleSearchStringChange": "handleSearchStringChange"
|
||||
}
|
||||
}
|
29
web/src/locales/zh.json
Normal file
29
web/src/locales/zh.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"libraryFilter": {
|
||||
"all": "全部",
|
||||
"repositoryFilter": "仓库筛选",
|
||||
"selectAll": "全选"
|
||||
},
|
||||
"language": "语言",
|
||||
"timeFilter": {
|
||||
"label": "时间筛选",
|
||||
"unlimited": "不限时间",
|
||||
"threeHours": "最近3小时",
|
||||
"today": "今天",
|
||||
"week": "最近一周",
|
||||
"month": "最近一个月",
|
||||
"threeMonths": "最近三个月",
|
||||
"custom": "自定义",
|
||||
"customRange": "{start} - {end}"
|
||||
},
|
||||
"searchPlaceholder": "输入关键词搜索或直接按回车键显示最新记录",
|
||||
"loading": "加载中...",
|
||||
"searchSummary": "✨ 找到 {found} 个结果 - 在 {time} 毫秒内搜索了 {outOf} 个记录。",
|
||||
"noResults": "未找到结果。",
|
||||
"copyright": "© 2024 Arkohut 漆妮妮 保留所有权利",
|
||||
"privacyPolicy": "隐私政策",
|
||||
"changelog": "更新日志",
|
||||
"searchLog": {
|
||||
"handleSearchStringChange": "处理搜索字符串变化"
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import '../i18n';
|
||||
import '../app.css';
|
||||
import { Toaster } from "$lib/components/ui/sonner";
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -10,6 +10,8 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { translateAppName } from '$lib/utils';
|
||||
import LucideIcon from '$lib/components/LucideIcon.svelte';
|
||||
import LanguageSwitcher from '$lib/LanguageSwitcher.svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
||||
let searchString = '';
|
||||
/**
|
||||
@ -64,7 +66,6 @@
|
||||
|
||||
onMount(() => {
|
||||
const handleScroll = () => {
|
||||
console.log(window.scrollY)
|
||||
if (window.scrollY > 100) {
|
||||
isScrolled = true;
|
||||
} else if (isScrolled && window.scrollY < 20) {
|
||||
@ -296,7 +297,7 @@
|
||||
type="text"
|
||||
class={inputClasses}
|
||||
bind:value={searchString}
|
||||
placeholder="Input keyword to search or press Enter to show latest records"
|
||||
placeholder={$_('searchPlaceholder')}
|
||||
on:keydown={handleEnterPress}
|
||||
autofocus
|
||||
/>
|
||||
@ -331,13 +332,15 @@
|
||||
<!-- Right panel for search results -->
|
||||
<div class="{searchResult && searchResult.facet_counts && searchResult.facet_counts.length > 0 ? 'xl:w-6/7 lg:w-5/6 md:w-4/5' : 'w-full'}">
|
||||
{#if isLoading}
|
||||
<p class="text-center">Loading...</p>
|
||||
<p class="text-center">{$_('loading')}</p>
|
||||
{:else if searchResult && searchResult.hits.length > 0}
|
||||
{#if searchResult['search_time_ms'] > 0}
|
||||
<p class="search-summary mb-4 text-center">
|
||||
✨ {searchResult['found'].toLocaleString()} results found - Searched {searchResult[
|
||||
'out_of'
|
||||
].toLocaleString()} recipes in {searchResult['search_time_ms']}ms.
|
||||
{$_('searchSummary', { values: {
|
||||
found: searchResult['found'].toLocaleString(),
|
||||
outOf: searchResult['out_of'].toLocaleString(),
|
||||
time: searchResult['search_time_ms']
|
||||
}})}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
@ -377,7 +380,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else if searchString}
|
||||
<p class="text-center">No results found.</p>
|
||||
<p class="text-center">{$_('noResults')}</p>
|
||||
{:else}
|
||||
<p class="text-center"></p>
|
||||
{/if}
|
||||
@ -407,11 +410,13 @@
|
||||
|
||||
<footer class="mx-auto mt-32 w-full container text-center">
|
||||
<div class="border-t border-slate-900/5 py-10">
|
||||
<p class="mt-2 text-sm leading-6 text-slate-500">© 2024 Arkohut Qinini. All rights reserved.</p>
|
||||
<p class="mt-2 text-sm leading-6 text-slate-500">{$_('copyright')}</p>
|
||||
<div class="mt-2 flex justify-center items-center space-x-4 text-sm font-semibold leading-6 text-slate-700">
|
||||
<a href="/privacy-policy">Privacy policy</a>
|
||||
<a href="/privacy-policy">{$_('privacyPolicy')}</a>
|
||||
<div class="h-4 w-px bg-slate-500/20" />
|
||||
<a href="/changelog">Changelog</a>
|
||||
<a href="/changelog">{$_('changelog')}</a>
|
||||
<div class="h-4 w-px bg-slate-500/20" />
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
Loading…
x
Reference in New Issue
Block a user