mirror of
https://github.com/tcsenpai/pensieve.git
synced 2025-06-07 03:35:24 +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">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { _ } from 'svelte-i18n'; // 导入翻译函数
|
||||||
|
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
|
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
|
||||||
@ -33,7 +34,7 @@
|
|||||||
let selectedLibraries: Record<number, boolean> = {};
|
let selectedLibraries: Record<number, boolean> = {};
|
||||||
let allSelected = true;
|
let allSelected = true;
|
||||||
|
|
||||||
let displayName = '全部';
|
let displayName = $_('libraryFilter.all');
|
||||||
|
|
||||||
let prevSelectedLibraryIds: number[] = [];
|
let prevSelectedLibraryIds: number[] = [];
|
||||||
|
|
||||||
@ -61,12 +62,12 @@
|
|||||||
.map((id) => libraries.find(lib => lib.id === id)?.name)
|
.map((id) => libraries.find(lib => lib.id === id)?.name)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
} else if (selectedCount === 0) {
|
} else if (selectedCount === 0) {
|
||||||
displayName = '全部';
|
displayName = $_('libraryFilter.all');
|
||||||
allSelected = true;
|
allSelected = true;
|
||||||
selectedLibraries = {};
|
selectedLibraries = {};
|
||||||
newSelectedLibraryIds = [];
|
newSelectedLibraryIds = [];
|
||||||
} else {
|
} else {
|
||||||
displayName = '全部';
|
displayName = $_('libraryFilter.all');
|
||||||
allSelected = true;
|
allSelected = true;
|
||||||
selectedLibraries = {};
|
selectedLibraries = {};
|
||||||
newSelectedLibraryIds = [];
|
newSelectedLibraryIds = [];
|
||||||
@ -98,13 +99,13 @@
|
|||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
<Popover.Content class="w-56 mt-1 p-1" align="start" side="bottom">
|
<Popover.Content class="w-56 mt-1 p-1" align="start" side="bottom">
|
||||||
<div class="px-2 py-1.5 text-sm font-semibold">
|
<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>
|
</div>
|
||||||
<Separator class="my-1" />
|
<Separator class="my-1" />
|
||||||
<div class="px-2 py-1.5">
|
<div class="px-2 py-1.5">
|
||||||
<div class="mb-2 items-top flex space-x-2">
|
<div class="mb-2 items-top flex space-x-2">
|
||||||
<Checkbox id="all-selected" bind:checked={allSelected} disabled={allSelected} onCheckedChange={toggleSelectAll} />
|
<Checkbox id="all-selected" bind:checked={allSelected} disabled={allSelected} onCheckedChange={toggleSelectAll} />
|
||||||
<Label for="all-selected" class="flex items-center text-sm">全选</Label>
|
<Label for="all-selected" class="flex items-center text-sm">{$_('libraryFilter.selectAll')}</Label>
|
||||||
</div>
|
</div>
|
||||||
{#each libraries as library}
|
{#each libraries as library}
|
||||||
<div class="mb-2 items-top flex space-x-2">
|
<div class="mb-2 items-top flex space-x-2">
|
||||||
@ -118,4 +119,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,49 +2,56 @@
|
|||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
import { RangeCalendar } from '$lib/components/ui/range-calendar/index.js';
|
import { RangeCalendar } from '$lib/components/ui/range-calendar/index.js';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CalendarDate,
|
|
||||||
DateFormatter,
|
|
||||||
type DateValue,
|
type DateValue,
|
||||||
getLocalTimeZone
|
getLocalTimeZone
|
||||||
} from '@internationalized/date';
|
} from '@internationalized/date';
|
||||||
|
|
||||||
let now = +new Date();
|
let now = +new Date();
|
||||||
|
|
||||||
const rangeMap = {
|
let rangeMap: {
|
||||||
|
[key: string]: {
|
||||||
|
label: string;
|
||||||
|
start: number | null;
|
||||||
|
end: number | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$: rangeMap = {
|
||||||
unlimited: {
|
unlimited: {
|
||||||
label: '不限时间',
|
label: $_('timeFilter.unlimited'),
|
||||||
start: null,
|
start: null,
|
||||||
end: null
|
end: null
|
||||||
},
|
},
|
||||||
threeHours: {
|
threeHours: {
|
||||||
label: '最近3小时',
|
label: $_('timeFilter.threeHours'),
|
||||||
start: now - 3 * 60 * 60 * 1000,
|
start: now - 3 * 60 * 60 * 1000,
|
||||||
end: now
|
end: now
|
||||||
},
|
},
|
||||||
today: {
|
today: {
|
||||||
label: '今天',
|
label: $_('timeFilter.today'),
|
||||||
start: now - 24 * 60 * 60 * 1000,
|
start: now - 24 * 60 * 60 * 1000,
|
||||||
end: now
|
end: now
|
||||||
},
|
},
|
||||||
week: {
|
week: {
|
||||||
label: '最近一周',
|
label: $_('timeFilter.week'),
|
||||||
start: now - 7 * 24 * 60 * 60 * 1000,
|
start: now - 7 * 24 * 60 * 60 * 1000,
|
||||||
end: now
|
end: now
|
||||||
},
|
},
|
||||||
month: {
|
month: {
|
||||||
label: '最近一个月',
|
label: $_('timeFilter.month'),
|
||||||
start: now - 30 * 24 * 60 * 60 * 1000,
|
start: now - 30 * 24 * 60 * 60 * 1000,
|
||||||
end: now
|
end: now
|
||||||
},
|
},
|
||||||
threeMonths: {
|
threeMonths: {
|
||||||
label: '最近三个月',
|
label: $_('timeFilter.threeMonths'),
|
||||||
start: now - 90 * 24 * 60 * 60 * 1000,
|
start: now - 90 * 24 * 60 * 60 * 1000,
|
||||||
end: now
|
end: now
|
||||||
},
|
},
|
||||||
custom: {
|
custom: {
|
||||||
label: '自定义',
|
label: $_('timeFilter.custom'),
|
||||||
start: null,
|
start: null,
|
||||||
end: null
|
end: null
|
||||||
}
|
}
|
||||||
@ -80,7 +87,7 @@
|
|||||||
|
|
||||||
$: displayText =
|
$: displayText =
|
||||||
(timeFilter === 'custom' && customDateRange.start && customDateRange.end)
|
(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;
|
: rangeMap[timeFilter].label;
|
||||||
$: start = rangeMap[timeFilter].start;
|
$: start = rangeMap[timeFilter].start;
|
||||||
$: end = rangeMap[timeFilter].end;
|
$: end = rangeMap[timeFilter].end;
|
||||||
@ -99,17 +106,17 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content class="w-56" align="start" side="bottom">
|
<DropdownMenu.Content class="w-56" align="start" side="bottom">
|
||||||
<DropdownMenu.Label>时间筛选</DropdownMenu.Label>
|
<DropdownMenu.Label>{$_('timeFilter.label')}</DropdownMenu.Label>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<DropdownMenu.RadioGroup bind:value={timeFilter}>
|
<DropdownMenu.RadioGroup bind:value={timeFilter}>
|
||||||
<DropdownMenu.RadioItem value="unlimited">时间不限</DropdownMenu.RadioItem>
|
<DropdownMenu.RadioItem value="unlimited">{$_('timeFilter.unlimited')}</DropdownMenu.RadioItem>
|
||||||
<DropdownMenu.RadioItem value="threeHours">最近三小时</DropdownMenu.RadioItem>
|
<DropdownMenu.RadioItem value="threeHours">{$_('timeFilter.threeHours')}</DropdownMenu.RadioItem>
|
||||||
<DropdownMenu.RadioItem value="today">今天</DropdownMenu.RadioItem>
|
<DropdownMenu.RadioItem value="today">{$_('timeFilter.today')}</DropdownMenu.RadioItem>
|
||||||
<DropdownMenu.RadioItem value="week">最近一周</DropdownMenu.RadioItem>
|
<DropdownMenu.RadioItem value="week">{$_('timeFilter.week')}</DropdownMenu.RadioItem>
|
||||||
<DropdownMenu.RadioItem value="month">最近一个月</DropdownMenu.RadioItem>
|
<DropdownMenu.RadioItem value="month">{$_('timeFilter.month')}</DropdownMenu.RadioItem>
|
||||||
<DropdownMenu.RadioItem value="threeMonths">最近三个月</DropdownMenu.RadioItem>
|
<DropdownMenu.RadioItem value="threeMonths">{$_('timeFilter.threeMonths')}</DropdownMenu.RadioItem>
|
||||||
<DropdownMenu.Sub>
|
<DropdownMenu.Sub>
|
||||||
<DropdownMenu.SubTrigger>自定义</DropdownMenu.SubTrigger>
|
<DropdownMenu.SubTrigger>{$_('timeFilter.custom')}</DropdownMenu.SubTrigger>
|
||||||
<DropdownMenu.SubContent>
|
<DropdownMenu.SubContent>
|
||||||
<RangeCalendar bind:value={customDateRange} />
|
<RangeCalendar bind:value={customDateRange} />
|
||||||
</DropdownMenu.SubContent>
|
</DropdownMenu.SubContent>
|
||||||
@ -117,4 +124,4 @@
|
|||||||
</DropdownMenu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</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">
|
<script lang="ts">
|
||||||
|
import '../i18n';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import { Toaster } from "$lib/components/ui/sonner";
|
import { Toaster } from '$lib/components/ui/sonner';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
@ -10,6 +10,8 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { translateAppName } from '$lib/utils';
|
import { translateAppName } from '$lib/utils';
|
||||||
import LucideIcon from '$lib/components/LucideIcon.svelte';
|
import LucideIcon from '$lib/components/LucideIcon.svelte';
|
||||||
|
import LanguageSwitcher from '$lib/LanguageSwitcher.svelte';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
let searchString = '';
|
let searchString = '';
|
||||||
/**
|
/**
|
||||||
@ -64,7 +66,6 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
console.log(window.scrollY)
|
|
||||||
if (window.scrollY > 100) {
|
if (window.scrollY > 100) {
|
||||||
isScrolled = true;
|
isScrolled = true;
|
||||||
} else if (isScrolled && window.scrollY < 20) {
|
} else if (isScrolled && window.scrollY < 20) {
|
||||||
@ -296,7 +297,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class={inputClasses}
|
class={inputClasses}
|
||||||
bind:value={searchString}
|
bind:value={searchString}
|
||||||
placeholder="Input keyword to search or press Enter to show latest records"
|
placeholder={$_('searchPlaceholder')}
|
||||||
on:keydown={handleEnterPress}
|
on:keydown={handleEnterPress}
|
||||||
autofocus
|
autofocus
|
||||||
/>
|
/>
|
||||||
@ -331,13 +332,15 @@
|
|||||||
<!-- Right panel for search results -->
|
<!-- 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'}">
|
<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}
|
{#if isLoading}
|
||||||
<p class="text-center">Loading...</p>
|
<p class="text-center">{$_('loading')}</p>
|
||||||
{:else if searchResult && searchResult.hits.length > 0}
|
{:else if searchResult && searchResult.hits.length > 0}
|
||||||
{#if searchResult['search_time_ms'] > 0}
|
{#if searchResult['search_time_ms'] > 0}
|
||||||
<p class="search-summary mb-4 text-center">
|
<p class="search-summary mb-4 text-center">
|
||||||
✨ {searchResult['found'].toLocaleString()} results found - Searched {searchResult[
|
{$_('searchSummary', { values: {
|
||||||
'out_of'
|
found: searchResult['found'].toLocaleString(),
|
||||||
].toLocaleString()} recipes in {searchResult['search_time_ms']}ms.
|
outOf: searchResult['out_of'].toLocaleString(),
|
||||||
|
time: searchResult['search_time_ms']
|
||||||
|
}})}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
@ -377,7 +380,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else if searchString}
|
{:else if searchString}
|
||||||
<p class="text-center">No results found.</p>
|
<p class="text-center">{$_('noResults')}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-center"></p>
|
<p class="text-center"></p>
|
||||||
{/if}
|
{/if}
|
||||||
@ -407,11 +410,13 @@
|
|||||||
|
|
||||||
<footer class="mx-auto mt-32 w-full container text-center">
|
<footer class="mx-auto mt-32 w-full container text-center">
|
||||||
<div class="border-t border-slate-900/5 py-10">
|
<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">
|
<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" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user