feat: i18n support

This commit is contained in:
arkohut 2024-10-17 18:05:51 +08:00
parent 0eb217aa24
commit 37fefef3ea
8 changed files with 155 additions and 38 deletions

9
web/src/i18n.ts Normal file
View 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', // 或者根据用户的浏览器设置动态选择
});

View 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>

View File

@ -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>

View File

@ -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
View 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
View 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": "处理搜索字符串变化"
}
}

View File

@ -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>

View File

@ -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>