One Calendar is a privacy-first calendar web app built with Next.js. It has modern security features, including e2ee, password-protected sharing, and self-destructing share links 📅 calendar.xyehr.cn
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 191 lines 5.0 kB view raw
1'use client' 2 3import { useEffect, useState } from 'react' 4import { 5 getEncryptionState, 6 readEncryptedLocalStorage, 7 subscribeEncryptionState, 8 writeEncryptedLocalStorage, 9} from '@/hooks/useLocalStorage' 10import { 11 translations as localeTranslations, 12 type Language, 13} from '@/lib/locales' 14 15const LANGUAGE_STORAGE_KEY = 'preferred-language' 16 17export const supportedLanguages = Object.keys(localeTranslations) as Language[] 18 19const baseLanguage = 'en' as const 20 21export const translations = Object.fromEntries( 22 supportedLanguages.map((lang) => [ 23 lang, 24 { 25 ...localeTranslations[baseLanguage], 26 ...localeTranslations[lang], 27 }, 28 ]), 29) as typeof localeTranslations 30 31const LANGUAGE_AUTONYM: Partial<Record<Language, string>> = { 32 en: 'English', 33 'en-GB': 'British English', 34 de: 'Deutsch', 35 es: 'Español', 36 fr: 'Français', 37 ja: '日本語', 38 yue: '粵語', 39 'zh-CN': '简体中文', 40 'zh-HK': '繁體中文(香港)', 41 'zh-TW': '繁體中文(台灣)', 42 it: 'Italiano', 43 ko: '한국어', 44 pl: 'Polski', 45 nl: 'Nederlands', 46 pt: 'Português', 47 ru: 'Русский', 48 sv: 'Svenska', 49 fi: 'Suomi', 50 hi: 'हिन्दी', 51 nb: 'Norsk bokmål', 52 vi: 'Tiếng Việt', 53 ro: 'Română', 54 uk: 'Українська', 55 is: 'Íslenska', 56 sw: 'Kiswahili', 57 bn: 'বাংলা', 58 el: 'Ελληνικά', 59 sq: 'Shqip', 60 lt: 'Lietuvių', 61 lv: 'Latviešu', 62 sl: 'Slovenščina', 63 mk: 'Македонски', 64 sr: 'Српски', 65} 66 67const byExactLowercase = new Map( 68 supportedLanguages.map((lang) => [lang.toLowerCase(), lang] as const), 69) 70 71const byBaseLowercase = new Map( 72 supportedLanguages.map( 73 (lang) => [lang.toLowerCase().split('-')[0], lang] as const, 74 ), 75) 76 77const normalizeLanguage = ( 78 value: string | null | undefined, 79): Language | null => { 80 if (!value) return null 81 82 const normalized = value.toLowerCase() 83 const exact = byExactLowercase.get(normalized) 84 if (exact) return exact 85 86 const base = normalized.split('-')[0] 87 return byBaseLowercase.get(base) ?? null 88} 89 90export const getLanguageAutonym = (language: Language) => { 91 const configured = LANGUAGE_AUTONYM[language] 92 if (configured) return configured 93 94 return ( 95 new Intl.DisplayNames([language], { type: 'language' }).of(language) ?? 96 language 97 ) 98} 99 100const zhLanguages: Language[] = ['zh-CN', 'zh-HK', 'zh-TW'] 101 102export const isZhLanguage = (language: Language) => 103 zhLanguages.includes(language) 104 105export const getStoredLanguage = async (): Promise<Language> => { 106 const storedLanguage = await readEncryptedLocalStorage<string | null>( 107 LANGUAGE_STORAGE_KEY, 108 null, 109 ) 110 return normalizeLanguage(storedLanguage) ?? detectSystemLanguage() 111} 112 113function detectSystemLanguage(): Language { 114 if (typeof window === 'undefined') { 115 return 'en' 116 } 117 118 const browserLang = navigator.language 119 return normalizeLanguage(browserLang) ?? 'en' 120} 121 122export function useLanguage(): [Language, (lang: Language) => void] { 123 const [language, setLanguageState] = useState<Language>('en') 124 125 useEffect(() => { 126 let active = true 127 const loadLanguage = () => 128 readEncryptedLocalStorage<string | null>(LANGUAGE_STORAGE_KEY, null).then( 129 (storedLanguage) => { 130 if (!active) return 131 const normalized = 132 normalizeLanguage(storedLanguage) ?? detectSystemLanguage() 133 setLanguageState(normalized) 134 if (storedLanguage && normalized !== storedLanguage) { 135 void writeEncryptedLocalStorage(LANGUAGE_STORAGE_KEY, normalized) 136 } 137 }, 138 ) 139 140 loadLanguage() 141 142 const handleStorageChange = (e: StorageEvent) => { 143 if (e.key === LANGUAGE_STORAGE_KEY) { 144 readEncryptedLocalStorage<string | null>( 145 LANGUAGE_STORAGE_KEY, 146 null, 147 ).then((newLanguage) => { 148 const normalized = normalizeLanguage(newLanguage) 149 if (normalized) { 150 setLanguageState(normalized) 151 } 152 }) 153 } 154 } 155 156 const handleCustomLanguageChange = (event: Event) => { 157 const customEvent = event as CustomEvent<{ language?: string }> 158 const normalized = normalizeLanguage(customEvent.detail?.language) 159 if (normalized) { 160 setLanguageState(normalized) 161 } 162 } 163 164 const unsubscribe = subscribeEncryptionState(() => { 165 if (getEncryptionState().ready) { 166 loadLanguage() 167 } 168 }) 169 170 window.addEventListener('storage', handleStorageChange) 171 window.addEventListener('languagechange', handleCustomLanguageChange) 172 return () => { 173 active = false 174 unsubscribe() 175 window.removeEventListener('storage', handleStorageChange) 176 window.removeEventListener('languagechange', handleCustomLanguageChange) 177 } 178 }, []) 179 180 const setLanguage = (lang: Language) => { 181 setLanguageState(lang) 182 void writeEncryptedLocalStorage(LANGUAGE_STORAGE_KEY, lang) 183 184 window.dispatchEvent( 185 new CustomEvent('languagechange', { detail: { language: lang } }), 186 ) 187 } 188 189 return [language, setLanguage] 190} 191export type { Language }