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

View File

@ -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
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">
import '../i18n';
import '../app.css';
import { Toaster } from "$lib/components/ui/sonner";
import { Toaster } from '$lib/components/ui/sonner';
</script>
<svelte:head>

View File

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