2024-04-09 16:21:05 +05:30
|
|
|
import clsx, { ClassValue } from 'clsx';
|
|
|
|
|
import { twMerge } from 'tailwind-merge';
|
|
|
|
|
|
|
|
|
|
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes));
|
2024-04-09 19:10:15 +05:30
|
|
|
|
feat(i18n): Integrate next-intl, localize core UI, add regional locales and zh-TW Discover sources
**Overview**
- Integrates next-intl (App Router, no i18n routing) with cookie-based locale and Accept-Language fallback.
- Adds message bundles and regional variants; sets en-US as the default.
**Key changes**
- i18n foundation
- Adds request-scoped config to load messages per locale and injects NextIntlClientProvider in [layout.tsx]
- Adds/updates messages for: en-US, en-GB, zh-TW, zh-HK, zh-CN, ja, ko, fr-FR, fr-CA, de.
Centralizes LOCALES, LOCALE_LABELS, and DEFAULT_LOCALE in [locales.ts]
- Adds LocaleSwitcher (cookie-based) and [LocaleBootstrap]
- Pages and components
- Localizes Sidebar, Home (including metadata/manifest), Settings, Discover, Library.
- Localizes common components: MessageInput, Attach, Focus, Optimization, MessageBox, MessageSources, SearchImages, SearchVideos, EmptyChat, NewsArticleWidget, WeatherWidget.
- APIs
- Weather API returns localized condition strings server-side.
- UX and quality
- Converts all remaining <img> to Next Image.
- Updates browserslist/caniuse DB to silence warnings.
- Security: Settings API Key inputs are now password fields and placeholders were removed.
2025-08-16 12:27:18 +08:00
|
|
|
// Locale-aware absolute date formatting
|
|
|
|
|
export const formatDate = (
|
|
|
|
|
date: Date | string,
|
|
|
|
|
locale: string,
|
|
|
|
|
options: Intl.DateTimeFormatOptions = {
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: 'short',
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
},
|
|
|
|
|
) => {
|
|
|
|
|
const d = new Date(date);
|
|
|
|
|
return new Intl.DateTimeFormat(locale || undefined, options).format(d);
|
|
|
|
|
};
|
|
|
|
|
|
2024-06-28 09:34:03 +05:30
|
|
|
export const formatTimeDifference = (
|
|
|
|
|
date1: Date | string,
|
|
|
|
|
date2: Date | string,
|
|
|
|
|
): string => {
|
|
|
|
|
date1 = new Date(date1);
|
|
|
|
|
date2 = new Date(date2);
|
|
|
|
|
|
2024-04-09 19:10:15 +05:30
|
|
|
const diffInSeconds = Math.floor(
|
|
|
|
|
Math.abs(date2.getTime() - date1.getTime()) / 1000,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (diffInSeconds < 60)
|
|
|
|
|
return `${diffInSeconds} second${diffInSeconds !== 1 ? 's' : ''}`;
|
|
|
|
|
else if (diffInSeconds < 3600)
|
|
|
|
|
return `${Math.floor(diffInSeconds / 60)} minute${Math.floor(diffInSeconds / 60) !== 1 ? 's' : ''}`;
|
|
|
|
|
else if (diffInSeconds < 86400)
|
|
|
|
|
return `${Math.floor(diffInSeconds / 3600)} hour${Math.floor(diffInSeconds / 3600) !== 1 ? 's' : ''}`;
|
|
|
|
|
else if (diffInSeconds < 31536000)
|
|
|
|
|
return `${Math.floor(diffInSeconds / 86400)} day${Math.floor(diffInSeconds / 86400) !== 1 ? 's' : ''}`;
|
|
|
|
|
else
|
|
|
|
|
return `${Math.floor(diffInSeconds / 31536000)} year${Math.floor(diffInSeconds / 31536000) !== 1 ? 's' : ''}`;
|
|
|
|
|
};
|
feat(i18n): Integrate next-intl, localize core UI, add regional locales and zh-TW Discover sources
**Overview**
- Integrates next-intl (App Router, no i18n routing) with cookie-based locale and Accept-Language fallback.
- Adds message bundles and regional variants; sets en-US as the default.
**Key changes**
- i18n foundation
- Adds request-scoped config to load messages per locale and injects NextIntlClientProvider in [layout.tsx]
- Adds/updates messages for: en-US, en-GB, zh-TW, zh-HK, zh-CN, ja, ko, fr-FR, fr-CA, de.
Centralizes LOCALES, LOCALE_LABELS, and DEFAULT_LOCALE in [locales.ts]
- Adds LocaleSwitcher (cookie-based) and [LocaleBootstrap]
- Pages and components
- Localizes Sidebar, Home (including metadata/manifest), Settings, Discover, Library.
- Localizes common components: MessageInput, Attach, Focus, Optimization, MessageBox, MessageSources, SearchImages, SearchVideos, EmptyChat, NewsArticleWidget, WeatherWidget.
- APIs
- Weather API returns localized condition strings server-side.
- UX and quality
- Converts all remaining <img> to Next Image.
- Updates browserslist/caniuse DB to silence warnings.
- Security: Settings API Key inputs are now password fields and placeholders were removed.
2025-08-16 12:27:18 +08:00
|
|
|
|
|
|
|
|
// Locale-aware relative time using Intl.RelativeTimeFormat
|
|
|
|
|
export const formatRelativeTime = (
|
|
|
|
|
date1: Date | string,
|
|
|
|
|
date2: Date | string,
|
|
|
|
|
locale: string,
|
|
|
|
|
): string => {
|
|
|
|
|
const d1 = new Date(date1);
|
|
|
|
|
const d2 = new Date(date2);
|
|
|
|
|
const diffSeconds = Math.floor((d2.getTime() - d1.getTime()) / 1000); // positive if d2 > d1
|
|
|
|
|
|
|
|
|
|
const abs = Math.abs(diffSeconds);
|
|
|
|
|
let value: number;
|
|
|
|
|
let unit: Intl.RelativeTimeFormatUnit;
|
|
|
|
|
|
|
|
|
|
if (abs < 60) {
|
|
|
|
|
value = Math.round(diffSeconds);
|
|
|
|
|
unit = 'second';
|
|
|
|
|
} else if (abs < 3600) {
|
|
|
|
|
value = Math.round(diffSeconds / 60);
|
|
|
|
|
unit = 'minute';
|
|
|
|
|
} else if (abs < 86400) {
|
|
|
|
|
value = Math.round(diffSeconds / 3600);
|
|
|
|
|
unit = 'hour';
|
|
|
|
|
} else if (abs < 2629800) {
|
|
|
|
|
// ~1 month (30.4 days)
|
|
|
|
|
value = Math.round(diffSeconds / 86400);
|
|
|
|
|
unit = 'day';
|
|
|
|
|
} else if (abs < 31557600) {
|
|
|
|
|
// ~1 year
|
|
|
|
|
value = Math.round(diffSeconds / 2629800);
|
|
|
|
|
unit = 'month';
|
|
|
|
|
} else {
|
|
|
|
|
value = Math.round(diffSeconds / 31557600);
|
|
|
|
|
unit = 'year';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rtf = new Intl.RelativeTimeFormat(locale || undefined, {
|
|
|
|
|
numeric: 'auto',
|
|
|
|
|
});
|
|
|
|
|
return rtf.format(value, unit);
|
|
|
|
|
};
|