Compare commits
	
		
			6 commits
		
	
	
		
			d3a7602fb5
			...
			69f7f37a2c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 69f7f37a2c | |||
|  | e6a14a8a26 | ||
|  | e56dc4bf7b | ||
|  | 55705b0a6d | ||
|  | c49ae6fe0a | ||
|  | b4fa751345 | 
					 15 changed files with 638 additions and 44 deletions
				
			
		
							
								
								
									
										
											BIN
										
									
								
								bun.lockb
									
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								bun.lockb
									
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -27,9 +27,11 @@ | ||||||
|     "wrangler": "^3.78.2" |     "wrangler": "^3.78.2" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|  |     "@formatjs/intl-localematcher": "^0.5.4", | ||||||
|     "@nanostores/persistent": "^0.10.2", |     "@nanostores/persistent": "^0.10.2", | ||||||
|     "@nanostores/solid": "^0.4.2", |     "@nanostores/solid": "^0.4.2", | ||||||
|     "@solid-primitives/event-listener": "^2.3.3", |     "@solid-primitives/event-listener": "^2.3.3", | ||||||
|  |     "@solid-primitives/i18n": "^2.1.1", | ||||||
|     "@solid-primitives/intersection-observer": "^2.1.6", |     "@solid-primitives/intersection-observer": "^2.1.6", | ||||||
|     "@solid-primitives/resize-observer": "^2.0.26", |     "@solid-primitives/resize-observer": "^2.0.26", | ||||||
|     "@solidjs/router": "^0.14.5", |     "@solidjs/router": "^0.14.5", | ||||||
|  | @ -40,6 +42,7 @@ | ||||||
|     "date-fns": "^3.6.0", |     "date-fns": "^3.6.0", | ||||||
|     "fast-average-color": "^9.4.0", |     "fast-average-color": "^9.4.0", | ||||||
|     "hammerjs": "^2.0.8", |     "hammerjs": "^2.0.8", | ||||||
|  |     "iso-639-1": "^3.1.3", | ||||||
|     "masto": "^6.8.0", |     "masto": "^6.8.0", | ||||||
|     "nanostores": "^0.11.3", |     "nanostores": "^0.11.3", | ||||||
|     "solid-js": "^1.8.22", |     "solid-js": "^1.8.22", | ||||||
|  |  | ||||||
							
								
								
									
										21
									
								
								src/App.tsx
									
										
									
									
									
								
							
							
						
						
									
										21
									
								
								src/App.tsx
									
										
									
									
									
								
							|  | @ -6,6 +6,7 @@ import { | ||||||
|   createSignal, |   createSignal, | ||||||
|   ErrorBoundary, |   ErrorBoundary, | ||||||
|   lazy, |   lazy, | ||||||
|  |   onCleanup, | ||||||
| } from "solid-js"; | } from "solid-js"; | ||||||
| import { useRootTheme } from "./material/mui.js"; | import { useRootTheme } from "./material/mui.js"; | ||||||
| import { | import { | ||||||
|  | @ -15,6 +16,7 @@ import { | ||||||
| } from "./masto/clients.js"; | } from "./masto/clients.js"; | ||||||
| import { $accounts } from "./accounts/stores.js"; | import { $accounts } from "./accounts/stores.js"; | ||||||
| import { useStore } from "@nanostores/solid"; | import { useStore } from "@nanostores/solid"; | ||||||
|  | import { DateFnScope, useLanguage } from "./platform/i18n.jsx"; | ||||||
| 
 | 
 | ||||||
| const AccountSignIn = lazy(() => import("./accounts/SignIn.js")); | const AccountSignIn = lazy(() => import("./accounts/SignIn.js")); | ||||||
| const AccountMastodonOAuth2Callback = lazy( | const AccountMastodonOAuth2Callback = lazy( | ||||||
|  | @ -47,6 +49,7 @@ const App: Component = () => { | ||||||
|   const theme = useRootTheme(); |   const theme = useRootTheme(); | ||||||
|   const accts = useStore($accounts); |   const accts = useStore($accounts); | ||||||
|   const clientStore = createSignal<Session[]>([]); |   const clientStore = createSignal<Session[]>([]); | ||||||
|  |   const lang = useLanguage(); | ||||||
| 
 | 
 | ||||||
|   createRenderEffect(() => { |   createRenderEffect(() => { | ||||||
|     const [, setClients] = clientStore; |     const [, setClients] = clientStore; | ||||||
|  | @ -55,6 +58,16 @@ const App: Component = () => { | ||||||
|     ); |     ); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   createRenderEffect(() => { | ||||||
|  |     const root = document.querySelector(":root")!; | ||||||
|  |     root.setAttribute("lang", lang()); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   onCleanup(() => { | ||||||
|  |     const root = document.querySelector(":root")!; | ||||||
|  |     root.removeAttribute("lang"); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   const UnexpectedError = lazy(() => import("./UnexpectedError.js")); |   const UnexpectedError = lazy(() => import("./UnexpectedError.js")); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|  | @ -65,9 +78,11 @@ const App: Component = () => { | ||||||
|       }} |       }} | ||||||
|     > |     > | ||||||
|       <ThemeProvider theme={theme()}> |       <ThemeProvider theme={theme()}> | ||||||
|         <ClientProvider value={clientStore}> |         <DateFnScope> | ||||||
|           <Routing /> |           <ClientProvider value={clientStore}> | ||||||
|         </ClientProvider> |             <Routing /> | ||||||
|  |           </ClientProvider> | ||||||
|  |         </DateFnScope> | ||||||
|       </ThemeProvider> |       </ThemeProvider> | ||||||
|     </ErrorBoundary> |     </ErrorBoundary> | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  | @ -12,11 +12,16 @@ export function useSignedInProfiles() { | ||||||
|   }); |   }); | ||||||
|   return [ |   return [ | ||||||
|     () => { |     () => { | ||||||
|       const value = accessor(); |       try { | ||||||
|       if (!value) { |         const value = accessor(); | ||||||
|         return sessions().map((x) => ({ ...x, inf: x.account.inf })); |         if (value) { | ||||||
|  |           return value; | ||||||
|  |         } | ||||||
|  |       } catch (reason) { | ||||||
|  |         console.error("useSignedInProfiles: update acct info failed", reason); | ||||||
|       } |       } | ||||||
|       return value; | 
 | ||||||
|  |       return sessions().map((x) => ({ ...x, inf: x.account.inf })); | ||||||
|     }, |     }, | ||||||
|     tools, |     tools, | ||||||
|   ] as const; |   ] as const; | ||||||
|  |  | ||||||
							
								
								
									
										188
									
								
								src/platform/i18n.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								src/platform/i18n.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,188 @@ | ||||||
|  | import { | ||||||
|  |   ParentComponent, | ||||||
|  |   createContext, | ||||||
|  |   createMemo, | ||||||
|  |   createResource, | ||||||
|  |   useContext, | ||||||
|  | } from "solid-js"; | ||||||
|  | import { match } from "@formatjs/intl-localematcher"; | ||||||
|  | import { Accessor, createEffect, createSignal } from "solid-js"; | ||||||
|  | import { $settings } from "../settings/stores"; | ||||||
|  | import { enGB } from "date-fns/locale/en-GB"; | ||||||
|  | import { useStore } from "@nanostores/solid"; | ||||||
|  | import type { Locale } from "date-fns"; | ||||||
|  | import { resolveTemplate, translator, type Template } from "@solid-primitives/i18n"; | ||||||
|  | 
 | ||||||
|  | async function synchronised( | ||||||
|  |   name: string, | ||||||
|  |   callback: () => Promise<void> | void, | ||||||
|  | ): Promise<void> { | ||||||
|  |   await navigator.locks.request(name, callback); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const SUPPORTED_LANGS = ["en", "zh-Hans"] as const; | ||||||
|  | 
 | ||||||
|  | export const SUPPORTED_REGIONS = ["en_US", "en_GB", "zh_CN"] as const; | ||||||
|  | 
 | ||||||
|  | const DEFAULT_LANG = "en"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Decide the using language for the user. | ||||||
|  |  * @returns the selected language tag | ||||||
|  |  */ | ||||||
|  | export function autoMatchLangTag() { | ||||||
|  |   return match(Array.from(navigator.languages), SUPPORTED_LANGS, DEFAULT_LANG); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const DateFnLocaleCx = /* __@PURE__ */createContext<Accessor<Locale>>(() => enGB); | ||||||
|  | 
 | ||||||
|  | const cachedDateFnLocale: Record<string, Locale> = { | ||||||
|  |   enGB, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export function autoMatchRegion() { | ||||||
|  |   const regions = navigator.languages | ||||||
|  |     .map((x) => { | ||||||
|  |       const parts = x.split("_"); | ||||||
|  |       if (parts.length > 1) { | ||||||
|  |         return parts[1]; | ||||||
|  |       } | ||||||
|  |       return undefined; | ||||||
|  |     }) | ||||||
|  |     .filter((x): x is string => !!x); | ||||||
|  |   for (const r of regions) { | ||||||
|  |     for (const available of SUPPORTED_REGIONS) { | ||||||
|  |       if (available.toLowerCase().endsWith(r.toLowerCase())) { | ||||||
|  |         return available; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return "en_GB"; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useRegion() { | ||||||
|  |   const appSettings = useStore($settings); | ||||||
|  | 
 | ||||||
|  |   return createMemo(() => { | ||||||
|  |     const settings = appSettings(); | ||||||
|  |     if (typeof settings.region !== "undefined") { | ||||||
|  |       return settings.region; | ||||||
|  |     } else { | ||||||
|  |       return autoMatchRegion(); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function importDateFnLocale(tag: string): Promise<Locale> { | ||||||
|  |   switch (tag.toLowerCase()) { | ||||||
|  |     case "en_us": | ||||||
|  |       return (await import("date-fns/locale/en-US")).enUS; | ||||||
|  |     case "en_gb": | ||||||
|  |       return (await import("date-fns/locale/en-GB")).enGB; | ||||||
|  |     case "zh_cn": | ||||||
|  |       return (await import("date-fns/locale/zh-CN")).zhCN; | ||||||
|  |     default: | ||||||
|  |       throw new TypeError(`unsupported tag "${tag}"`); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Provides runtime values and fetch dependencies for date-fns locale | ||||||
|  |  */ | ||||||
|  | export const DateFnScope: ParentComponent = (props) => { | ||||||
|  |   const [dateFnLocale, setDateFnLocale] = createSignal(enGB); | ||||||
|  |   const region = useRegion(); | ||||||
|  | 
 | ||||||
|  |   createEffect(() => { | ||||||
|  |     const dateFnLocaleName = region(); | ||||||
|  | 
 | ||||||
|  |     if (cachedDateFnLocale[dateFnLocaleName]) { | ||||||
|  |       setDateFnLocale(cachedDateFnLocale[dateFnLocaleName]); | ||||||
|  |     } else { | ||||||
|  |       synchronised("i18n-wrapper-load-date-fns-locale", async () => { | ||||||
|  |         if (cachedDateFnLocale[dateFnLocaleName]) { | ||||||
|  |           setDateFnLocale(cachedDateFnLocale[dateFnLocaleName]); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         const target = `date-fns/locale/${dateFnLocaleName}`; | ||||||
|  |         try { | ||||||
|  |           const mod = await importDateFnLocale(dateFnLocaleName); | ||||||
|  |           cachedDateFnLocale[dateFnLocaleName] = mod; | ||||||
|  |           setDateFnLocale(mod); | ||||||
|  |         } catch (reason) { | ||||||
|  |           console.error( | ||||||
|  |             { | ||||||
|  |               act: "load-date-fns-locale", | ||||||
|  |               stat: "failed", | ||||||
|  |               reason, | ||||||
|  |               target, | ||||||
|  |             }, | ||||||
|  |             "failed to load date-fns locale", | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <DateFnLocaleCx.Provider value={dateFnLocale}> | ||||||
|  |       {props.children} | ||||||
|  |     </DateFnLocaleCx.Provider> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Get the {@link Locale} object for date-fns. | ||||||
|  |  * | ||||||
|  |  * This function must be using in {@link DateFnScope} | ||||||
|  |  * | ||||||
|  |  * @returns Accessor for Locale | ||||||
|  |  */ | ||||||
|  | export function useDateFnLocale(): Accessor<Locale> { | ||||||
|  |   const cx = useContext(DateFnLocaleCx); | ||||||
|  |   return cx; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useLanguage() { | ||||||
|  |   const settings = useStore($settings); | ||||||
|  |   return () => settings().language || autoMatchLangTag(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type ImportFn<T> = (name: string) => Promise<{default: T}> | ||||||
|  | 
 | ||||||
|  | type ImportedModule<F> = F extends ImportFn<infer T> ? T: never | ||||||
|  | 
 | ||||||
|  | type MergedImportedModule<T> = | ||||||
|  |   T extends [] ? {} : | ||||||
|  |   T extends [infer I] ? ImportedModule<I> : | ||||||
|  |   T extends [infer I, ...infer J] ? ImportedModule<I> & MergedImportedModule<J> : never | ||||||
|  | 
 | ||||||
|  | export function createStringResource< | ||||||
|  |   T extends ImportFn<Record<string, string | Template<any> | undefined>>[], | ||||||
|  | >(...importFns: T) { | ||||||
|  |   const language = useLanguage(); | ||||||
|  |   const cache: Record<string, MergedImportedModule<T>> = {}; | ||||||
|  | 
 | ||||||
|  |   return createResource( | ||||||
|  |     () => [language()] as const, | ||||||
|  |     async ([nlang]) => { | ||||||
|  |       if (cache[nlang]) { | ||||||
|  |         return cache[nlang]; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const results = await Promise.all(importFns.map(x => x(nlang).then(v => v.default))) | ||||||
|  | 
 | ||||||
|  |       const merged: MergedImportedModule<T> = Object.assign({}, ...results) | ||||||
|  | 
 | ||||||
|  |       cache[nlang] = merged; | ||||||
|  | 
 | ||||||
|  |       return merged; | ||||||
|  |     }, | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function createTranslator<T extends ImportFn<Record<string, string | Template<any> | undefined>>[],>(...importFns: T) { | ||||||
|  |   const res = createStringResource(...importFns) | ||||||
|  | 
 | ||||||
|  |   return [translator(res[0], resolveTemplate), res] as const | ||||||
|  | } | ||||||
							
								
								
									
										116
									
								
								src/settings/ChooseLang.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/settings/ChooseLang.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,116 @@ | ||||||
|  | import { createMemo, For, type Component, type JSX } from "solid-js"; | ||||||
|  | import Scaffold from "../material/Scaffold"; | ||||||
|  | import { | ||||||
|  |   AppBar, | ||||||
|  |   IconButton, | ||||||
|  |   List, | ||||||
|  |   ListItem, | ||||||
|  |   ListItemButton, | ||||||
|  |   ListItemSecondaryAction, | ||||||
|  |   ListItemText, | ||||||
|  |   ListSubheader, | ||||||
|  |   Radio, | ||||||
|  |   Switch, | ||||||
|  |   Toolbar, | ||||||
|  | } from "@suid/material"; | ||||||
|  | import { Close as CloseIcon } from "@suid/icons-material"; | ||||||
|  | import iso639_1 from "iso-639-1"; | ||||||
|  | import { | ||||||
|  |   autoMatchLangTag, | ||||||
|  |   createTranslator, | ||||||
|  |   SUPPORTED_LANGS, | ||||||
|  | } from "../platform/i18n"; | ||||||
|  | import { Title } from "../material/typography"; | ||||||
|  | import type { Template } from "@solid-primitives/i18n"; | ||||||
|  | 
 | ||||||
|  | type ChooseLangProps = { | ||||||
|  |   code?: string; | ||||||
|  |   onCodeChange: (ncode?: string) => void; | ||||||
|  |   onClose?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const ChooseLang: Component<ChooseLangProps> = (props) => { | ||||||
|  |   const [t] = createTranslator( | ||||||
|  |     () => import("./i18n/lang-names.json"), | ||||||
|  |     (code) => | ||||||
|  |       import(`./i18n/${code}.json`) as Promise<{ | ||||||
|  |         default: Record<string, string | undefined> & { | ||||||
|  |           ["lang.auto"]: Template<{ detected: string }>; | ||||||
|  |         }; | ||||||
|  |       }>, | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const unsupportedLangCodes = createMemo(() => { | ||||||
|  |     return iso639_1.getAllCodes().filter((x) => !["zh", "en"].includes(x)); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   const matchedLangCode = createMemo(() => autoMatchLangTag()); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <Scaffold | ||||||
|  |       topbar={ | ||||||
|  |         <AppBar position="static"> | ||||||
|  |           <Toolbar | ||||||
|  |             variant="dense" | ||||||
|  |             sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }} | ||||||
|  |           > | ||||||
|  |             <IconButton color="inherit" onClick={props.onClose} disableRipple> | ||||||
|  |               <CloseIcon /> | ||||||
|  |             </IconButton> | ||||||
|  |             <Title>{t("Choose Language")}</Title> | ||||||
|  |           </Toolbar> | ||||||
|  |         </AppBar> | ||||||
|  |       } | ||||||
|  |     > | ||||||
|  |       <List | ||||||
|  |         sx={{ | ||||||
|  |           paddingBottom: "var(--safe-area-inset-bottom, 0)", | ||||||
|  |         }} | ||||||
|  |       > | ||||||
|  |         <ListItemButton | ||||||
|  |           onClick={() => { | ||||||
|  |             props.onCodeChange(props.code ? undefined : matchedLangCode()); | ||||||
|  |           }} | ||||||
|  |         > | ||||||
|  |           <ListItemText> | ||||||
|  |             {t("lang.auto", { | ||||||
|  |               detected: t(`lang.${matchedLangCode()}`) ?? matchedLangCode(), | ||||||
|  |             })} | ||||||
|  |           </ListItemText> | ||||||
|  |           <ListItemSecondaryAction> | ||||||
|  |             <Switch checked={typeof props.code === "undefined"} /> | ||||||
|  |           </ListItemSecondaryAction> | ||||||
|  |         </ListItemButton> | ||||||
|  |         <List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}> | ||||||
|  |           <For each={SUPPORTED_LANGS}> | ||||||
|  |             {(code) => ( | ||||||
|  |               <ListItemButton | ||||||
|  |                 disabled={typeof props.code === "undefined"} | ||||||
|  |                 onClick={[props.onCodeChange, code]} | ||||||
|  |               > | ||||||
|  |                 <ListItemText>{t(`lang.${code}`)}</ListItemText> | ||||||
|  |                 <ListItemSecondaryAction> | ||||||
|  |                   <Radio | ||||||
|  |                     checked={props.code === code || (props.code === undefined && matchedLangCode() == code)} | ||||||
|  |                   /> | ||||||
|  |                 </ListItemSecondaryAction> | ||||||
|  |               </ListItemButton> | ||||||
|  |             )} | ||||||
|  |           </For> | ||||||
|  |         </List> | ||||||
|  | 
 | ||||||
|  |         <List subheader={<ListSubheader>{t("Unsupported")}</ListSubheader>}> | ||||||
|  |           <For each={unsupportedLangCodes()}> | ||||||
|  |             {(code) => ( | ||||||
|  |               <ListItem> | ||||||
|  |                 <ListItemText>{iso639_1.getNativeName(code)}</ListItemText> | ||||||
|  |               </ListItem> | ||||||
|  |             )} | ||||||
|  |           </For> | ||||||
|  |         </List> | ||||||
|  |       </List> | ||||||
|  |     </Scaffold> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default ChooseLang; | ||||||
							
								
								
									
										106
									
								
								src/settings/ChooseRegion.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/settings/ChooseRegion.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,106 @@ | ||||||
|  | import { createMemo, For, type Component, type JSX } from "solid-js"; | ||||||
|  | import Scaffold from "../material/Scaffold"; | ||||||
|  | import { | ||||||
|  |   AppBar, | ||||||
|  |   IconButton, | ||||||
|  |   List, | ||||||
|  |   ListItemButton, | ||||||
|  |   ListItemSecondaryAction, | ||||||
|  |   ListItemText, | ||||||
|  |   ListSubheader, | ||||||
|  |   Radio, | ||||||
|  |   Switch, | ||||||
|  |   Toolbar, | ||||||
|  | } from "@suid/material"; | ||||||
|  | import { Close as CloseIcon } from "@suid/icons-material"; | ||||||
|  | import iso639_1 from "iso-639-1"; | ||||||
|  | import { | ||||||
|  |   autoMatchRegion, | ||||||
|  |   createTranslator, | ||||||
|  |   SUPPORTED_REGIONS, | ||||||
|  | } from "../platform/i18n"; | ||||||
|  | import { Title } from "../material/typography"; | ||||||
|  | import type { Template } from "@solid-primitives/i18n"; | ||||||
|  | 
 | ||||||
|  | type ChooseRegionProps = { | ||||||
|  |   code?: string; | ||||||
|  |   onCodeChange: (ncode?: string) => void; | ||||||
|  |   onClose?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const ChooseRegion: Component<ChooseRegionProps> = (props) => { | ||||||
|  |   const [t] = createTranslator( | ||||||
|  |     () => import("./i18n/lang-names.json"), | ||||||
|  |     (code) => | ||||||
|  |       import(`./i18n/${code}.json`) as Promise<{ | ||||||
|  |         default: Record<string, string | undefined> & { | ||||||
|  |           ["lang.auto"]: Template<{ detected: string }>; | ||||||
|  |         }; | ||||||
|  |       }>, | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const unsupportedLangCodes = createMemo(() => { | ||||||
|  |     return iso639_1.getAllCodes().filter((x) => !["zh", "en"].includes(x)); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   const matchedRegionCode = createMemo(() => autoMatchRegion()); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <Scaffold | ||||||
|  |       topbar={ | ||||||
|  |         <AppBar position="static"> | ||||||
|  |           <Toolbar | ||||||
|  |             variant="dense" | ||||||
|  |             sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }} | ||||||
|  |           > | ||||||
|  |             <IconButton color="inherit" onClick={props.onClose} disableRipple> | ||||||
|  |               <CloseIcon /> | ||||||
|  |             </IconButton> | ||||||
|  |             <Title>{t("Choose Language")}</Title> | ||||||
|  |           </Toolbar> | ||||||
|  |         </AppBar> | ||||||
|  |       } | ||||||
|  |     > | ||||||
|  |       <List | ||||||
|  |         sx={{ | ||||||
|  |           paddingBottom: "var(--safe-area-inset-bottom, 0)", | ||||||
|  |         }} | ||||||
|  |       > | ||||||
|  |         <ListItemButton | ||||||
|  |           onClick={() => { | ||||||
|  |             props.onCodeChange(props.code ? undefined : matchedRegionCode()); | ||||||
|  |           }} | ||||||
|  |         > | ||||||
|  |           <ListItemText> | ||||||
|  |             {t("region.auto", { | ||||||
|  |               detected: t(`region.${matchedRegionCode()}`) ?? matchedRegionCode(), | ||||||
|  |             })} | ||||||
|  |           </ListItemText> | ||||||
|  |           <ListItemSecondaryAction> | ||||||
|  |             <Switch checked={typeof props.code === "undefined"} /> | ||||||
|  |           </ListItemSecondaryAction> | ||||||
|  |         </ListItemButton> | ||||||
|  | 
 | ||||||
|  |         <List subheader={<ListSubheader>{t("Supported")}</ListSubheader>}> | ||||||
|  |           <For each={SUPPORTED_REGIONS}> | ||||||
|  |             {(code) => ( | ||||||
|  |               <ListItemButton | ||||||
|  |                 disabled={typeof props.code === "undefined"} | ||||||
|  |                 onClick={[props.onCodeChange, code]} | ||||||
|  |               > | ||||||
|  |                 <ListItemText>{t(`region.${code}`)}</ListItemText> | ||||||
|  |                 <ListItemSecondaryAction> | ||||||
|  |                   <Radio | ||||||
|  |                     checked={props.code === code || (props.code === undefined && matchedRegionCode() == code)} | ||||||
|  |                   /> | ||||||
|  |                 </ListItemSecondaryAction> | ||||||
|  |               </ListItemButton> | ||||||
|  |             )} | ||||||
|  |           </For> | ||||||
|  |         </List> | ||||||
|  |       </List> | ||||||
|  |     </Scaffold> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default ChooseRegion; | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { For, Show, type ParentComponent } from "solid-js"; | import { createSignal, For, Show, type ParentComponent } from "solid-js"; | ||||||
| import Scaffold from "../material/Scaffold.js"; | import Scaffold from "../material/Scaffold.js"; | ||||||
| import { | import { | ||||||
|   AppBar, |   AppBar, | ||||||
|  | @ -7,32 +7,63 @@ import { | ||||||
|   List, |   List, | ||||||
|   ListItem, |   ListItem, | ||||||
|   ListItemButton, |   ListItemButton, | ||||||
|  |   ListItemIcon, | ||||||
|   ListItemSecondaryAction, |   ListItemSecondaryAction, | ||||||
|   ListItemText, |   ListItemText, | ||||||
|   ListSubheader, |   ListSubheader, | ||||||
|  |   NativeSelect, | ||||||
|   Switch, |   Switch, | ||||||
|   Toolbar, |   Toolbar, | ||||||
| } from "@suid/material"; | } from "@suid/material"; | ||||||
| import { | import { | ||||||
|   Close as CloseIcon, |   Close as CloseIcon, | ||||||
|  |   Logout, | ||||||
|  |   Public as PublicIcon, | ||||||
|   Refresh as RefreshIcon, |   Refresh as RefreshIcon, | ||||||
|  |   Translate as TranslateIcon, | ||||||
| } from "@suid/icons-material"; | } from "@suid/icons-material"; | ||||||
| import { useNavigate } from "@solidjs/router"; | import { useNavigate } from "@solidjs/router"; | ||||||
| import { Title } from "../material/typography.jsx"; | import { Title } from "../material/typography.jsx"; | ||||||
| import { css } from "solid-styled"; | import { css } from "solid-styled"; | ||||||
| import { useSignedInProfiles } from "../masto/acct.js"; | import { useSignedInProfiles } from "../masto/acct.js"; | ||||||
| import { signOut, type Account } from "../accounts/stores.js"; | import { signOut, type Account } from "../accounts/stores.js"; | ||||||
| import { intlFormat } from "date-fns"; | import { format } from "date-fns"; | ||||||
| import { useStore } from "@nanostores/solid"; | import { useStore } from "@nanostores/solid"; | ||||||
| import { $settings } from "./stores.js"; | import { $settings } from "./stores.js"; | ||||||
| import { useRegisterSW } from "virtual:pwa-register/solid"; | import { useRegisterSW } from "virtual:pwa-register/solid"; | ||||||
|  | import { | ||||||
|  |   autoMatchLangTag, | ||||||
|  |   autoMatchRegion, | ||||||
|  |   createTranslator, | ||||||
|  |   SUPPORTED_LANGS, | ||||||
|  |   SUPPORTED_REGIONS, | ||||||
|  |   useDateFnLocale, | ||||||
|  | } from "../platform/i18n.jsx"; | ||||||
|  | import { type Template } from "@solid-primitives/i18n"; | ||||||
|  | import BottomSheet from "../material/BottomSheet.jsx"; | ||||||
|  | import ChooseLang from "./ChooseLang.jsx"; | ||||||
|  | import ChooseRegion from "./ChooseRegion.jsx"; | ||||||
|  | 
 | ||||||
|  | type Strings = { | ||||||
|  |   ["lang.auto"]: Template<{ detected: string }>; | ||||||
|  | } & Record<string, string | undefined>; | ||||||
| 
 | 
 | ||||||
| const Settings: ParentComponent = () => { | const Settings: ParentComponent = () => { | ||||||
|  |   const [t] = createTranslator( | ||||||
|  |     (code) => | ||||||
|  |       import(`./i18n/${code}.json`) as Promise<{ | ||||||
|  |         default: Strings; | ||||||
|  |       }>, | ||||||
|  |     () => import(`./i18n/lang-names.json`), | ||||||
|  |   ); | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const settings$ = useStore($settings); |   const settings$ = useStore($settings); | ||||||
|   const { |   const { | ||||||
|     needRefresh: [needRefresh], |     needRefresh: [needRefresh], | ||||||
|   } = useRegisterSW(); |   } = useRegisterSW(); | ||||||
|  |   const dateFnLocale = useDateFnLocale(); | ||||||
|  |   const [langPickerOpen, setLangPickerOpen] = createSignal(false); | ||||||
|  |   const [regionPickerOpen, setRegionPickerOpen] = createSignal(false); | ||||||
| 
 | 
 | ||||||
|   const [profiles] = useSignedInProfiles(); |   const [profiles] = useSignedInProfiles(); | ||||||
| 
 | 
 | ||||||
|  | @ -60,7 +91,7 @@ const Settings: ParentComponent = () => { | ||||||
|             <IconButton color="inherit" onClick={[navigate, -1]} disableRipple> |             <IconButton color="inherit" onClick={[navigate, -1]} disableRipple> | ||||||
|               <CloseIcon /> |               <CloseIcon /> | ||||||
|             </IconButton> |             </IconButton> | ||||||
|             <Title>Settings</Title> |             <Title>{t("Settings")}</Title> | ||||||
|           </Toolbar> |           </Toolbar> | ||||||
|         </AppBar> |         </AppBar> | ||||||
|       } |       } | ||||||
|  | @ -68,32 +99,35 @@ const Settings: ParentComponent = () => { | ||||||
|       <List class="setting-list" use:solid-styled> |       <List class="setting-list" use:solid-styled> | ||||||
|         <li> |         <li> | ||||||
|           <ul> |           <ul> | ||||||
|             <ListSubheader>Accounts</ListSubheader> |             <ListSubheader>{t("Accounts")}</ListSubheader> | ||||||
|             <ListItem> |             <ListItemButton disabled> | ||||||
|               <ListItemText>All Notifications</ListItemText> |               <ListItemText>{t("All Notifications")}</ListItemText> | ||||||
|               <ListItemSecondaryAction> |               <ListItemSecondaryAction> | ||||||
|                 <Switch value={false} /> |                 <Switch value={false} disabled /> | ||||||
|               </ListItemSecondaryAction> |               </ListItemSecondaryAction> | ||||||
|             </ListItem> |             </ListItemButton> | ||||||
|             <Divider /> |             <Divider /> | ||||||
|             <ListItem> |             <ListItemButton disabled> | ||||||
|               <ListItemText>Sign in...</ListItemText> |               <ListItemText>{t("Sign in...")}</ListItemText> | ||||||
|             </ListItem> |             </ListItemButton> | ||||||
|             <Divider /> |             <Divider /> | ||||||
|           </ul> |           </ul> | ||||||
|           <For each={profiles()}> |           <For each={profiles()}> | ||||||
|             {({ account: acct, inf }) => ( |             {({ account: acct, inf }) => ( | ||||||
|               <ul data-site={acct.site} data-username={inf?.username}> |               <ul data-site={acct.site} data-username={inf?.username}> | ||||||
|                 <ListSubheader>{`@${inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader> |                 <ListSubheader>{`@${inf?.username ?? "..."}@${new URL(acct.site).host}`}</ListSubheader> | ||||||
|                 <ListItem> |                 <ListItemButton disabled> | ||||||
|                   <ListItemText>Notifications</ListItemText> |                   <ListItemText>{t("Notifications")}</ListItemText> | ||||||
|                   <ListItemSecondaryAction> |                   <ListItemSecondaryAction> | ||||||
|                     <Switch value={false} /> |                     <Switch value={false} disabled /> | ||||||
|                   </ListItemSecondaryAction> |                   </ListItemSecondaryAction> | ||||||
|                 </ListItem> |                 </ListItemButton> | ||||||
|                 <Divider /> |                 <Divider /> | ||||||
|                 <ListItemButton onClick={[doSignOut, acct]}> |                 <ListItemButton onClick={[doSignOut, acct]}> | ||||||
|                   <ListItemText>Sign out</ListItemText> |                   <ListItemIcon> | ||||||
|  |                     <Logout /> | ||||||
|  |                   </ListItemIcon> | ||||||
|  |                   <ListItemText>{t("Sign out")}</ListItemText> | ||||||
|                 </ListItemButton> |                 </ListItemButton> | ||||||
|                 <Divider /> |                 <Divider /> | ||||||
|               </ul> |               </ul> | ||||||
|  | @ -101,12 +135,8 @@ const Settings: ParentComponent = () => { | ||||||
|           </For> |           </For> | ||||||
|         </li> |         </li> | ||||||
|         <li> |         <li> | ||||||
|           <ListSubheader>Reading</ListSubheader> |           <ListSubheader>{t("Reading")}</ListSubheader> | ||||||
|           <ListItem> |           <ListItemButton | ||||||
|             <ListItemText secondary="Regular">Fonts</ListItemText> |  | ||||||
|           </ListItem> |  | ||||||
|           <Divider /> |  | ||||||
|           <ListItem |  | ||||||
|             onClick={(e) => |             onClick={(e) => | ||||||
|               $settings.setKey( |               $settings.setKey( | ||||||
|                 "prefetchTootsDisabled", |                 "prefetchTootsDisabled", | ||||||
|  | @ -114,34 +144,93 @@ const Settings: ParentComponent = () => { | ||||||
|               ) |               ) | ||||||
|             } |             } | ||||||
|           > |           > | ||||||
|             <ListItemText secondary="Tutu will download toots before you scroll to the position."> |             <ListItemText secondary={t("Prefetch Toots.2nd")}> | ||||||
|               Prefetch Toots |               {t("Prefetch Toots")} | ||||||
|             </ListItemText> |             </ListItemText> | ||||||
|             <ListItemSecondaryAction> |             <ListItemSecondaryAction> | ||||||
|               <Switch checked={!settings$().prefetchTootsDisabled} /> |               <Switch checked={!settings$().prefetchTootsDisabled} /> | ||||||
|             </ListItemSecondaryAction> |             </ListItemSecondaryAction> | ||||||
|           </ListItem> |           </ListItemButton> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|         </li> |         </li> | ||||||
|         <li> |         <li> | ||||||
|           <ListSubheader>This Application</ListSubheader> |           <ListSubheader>{t("This Application")}</ListSubheader> | ||||||
|  |           <ListItemButton onClick={[setLangPickerOpen, true]}> | ||||||
|  |             <ListItemIcon> | ||||||
|  |               <TranslateIcon /> | ||||||
|  |             </ListItemIcon> | ||||||
|  |             <ListItemText | ||||||
|  |               secondary={ | ||||||
|  |                 settings$().language === undefined | ||||||
|  |                   ? t("lang.auto", { | ||||||
|  |                       detected: | ||||||
|  |                         t("lang." + autoMatchLangTag()) ?? autoMatchLangTag(), | ||||||
|  |                     }) | ||||||
|  |                   : t("lang." + settings$().language) | ||||||
|  |               } | ||||||
|  |             > | ||||||
|  |               {t("Language")} | ||||||
|  |             </ListItemText> | ||||||
|  |           </ListItemButton> | ||||||
|  |           <BottomSheet open={langPickerOpen()}> | ||||||
|  |             <ChooseLang | ||||||
|  |               code={settings$().language} | ||||||
|  |               onCodeChange={(nval) => $settings.setKey("language", nval)} | ||||||
|  |               onClose={[setLangPickerOpen, false]} | ||||||
|  |             /> | ||||||
|  |           </BottomSheet> | ||||||
|  |           <Divider /> | ||||||
|  |           <ListItemButton onClick={[setRegionPickerOpen, true]}> | ||||||
|  |             <ListItemIcon> | ||||||
|  |               <PublicIcon /> | ||||||
|  |             </ListItemIcon> | ||||||
|  |             <ListItemText | ||||||
|  |               secondary={ | ||||||
|  |                 settings$().region === undefined | ||||||
|  |                   ? t("region.auto", { | ||||||
|  |                       detected: | ||||||
|  |                         t("region." + autoMatchRegion()) ?? autoMatchRegion(), | ||||||
|  |                     }) | ||||||
|  |                   : t("region." + settings$().region) | ||||||
|  |               } | ||||||
|  |             > | ||||||
|  |               {t("Region")} | ||||||
|  |             </ListItemText> | ||||||
|  |           </ListItemButton> | ||||||
|  |           <BottomSheet open={regionPickerOpen()}> | ||||||
|  |             <ChooseRegion | ||||||
|  |               code={settings$().region} | ||||||
|  |               onCodeChange={(nval) => $settings.setKey("region", nval)} | ||||||
|  |               onClose={[setRegionPickerOpen, false]} | ||||||
|  |             /> | ||||||
|  |           </BottomSheet> | ||||||
|  |           <Divider /> | ||||||
|           <ListItem> |           <ListItem> | ||||||
|             <ListItemText secondary="Comformtable tooting experience."> |             <ListItemText secondary={t("About Tutu.2nd")}> | ||||||
|               About Tutu |               {t("About Tutu")} | ||||||
|             </ListItemText> |             </ListItemText> | ||||||
|           </ListItem> |           </ListItem> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|           <ListItem> |           <ListItem> | ||||||
|             <ListItemText |             <ListItemText | ||||||
|               secondary={`Using v${import.meta.env.PACKAGE_VERSION} (built on ${intlFormat(import.meta.env.BUILT_AT)}, ${import.meta.env.MODE})`} |               secondary={t("version", { | ||||||
|  |                 packageVersion: import.meta.env.PACKAGE_VERSION, | ||||||
|  |                 builtAt: format( | ||||||
|  |                   import.meta.env.BUILT_AT, | ||||||
|  |                   t("datefmt") || "yyyy/MM/dd", | ||||||
|  |                   { locale: dateFnLocale() }, | ||||||
|  |                 ), | ||||||
|  |                 buildMode: import.meta.env.MODE, | ||||||
|  |               })} | ||||||
|             > |             > | ||||||
|               {needRefresh() |               {needRefresh() ? t("updates.ready") : t("updates.no")} | ||||||
|                 ? "An update is ready, restart the Tutu to apply" |  | ||||||
|                 : "No updates"} |  | ||||||
|             </ListItemText> |             </ListItemText> | ||||||
|             <Show when={needRefresh()}> |             <Show when={needRefresh()}> | ||||||
|               <ListItemSecondaryAction> |               <ListItemSecondaryAction> | ||||||
|                 <IconButton aria-label="Restart Now" onClick={() => window.location.reload()}> |                 <IconButton | ||||||
|  |                   aria-label="Restart Now" | ||||||
|  |                   onClick={() => window.location.reload()} | ||||||
|  |                 > | ||||||
|                   <RefreshIcon /> |                   <RefreshIcon /> | ||||||
|                 </IconButton> |                 </IconButton> | ||||||
|               </ListItemSecondaryAction> |               </ListItemSecondaryAction> | ||||||
|  |  | ||||||
							
								
								
									
										30
									
								
								src/settings/i18n/en.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/settings/i18n/en.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | ||||||
|  | { | ||||||
|  |   "Settings": "Settings", | ||||||
|  |   "Accounts": "Accounts", | ||||||
|  |   "All Notifications": "All Notifications", | ||||||
|  |   "Sign in...": "Sign in...", | ||||||
|  |   "Reading": "Reading", | ||||||
|  |   "Prefetch Toots": "Prefetch Toots", | ||||||
|  |   "Prefetch Toots.2nd": "Tutu will download the toots before you need.", | ||||||
|  |   "This Application": "This Application", | ||||||
|  |   "About Tutu": "About Tutu", | ||||||
|  |   "About Tutu.2nd": "Comfortable tooting experience.", | ||||||
|  |   "updates.ready": "An update is ready, restart the Tutu to apply", | ||||||
|  |   "updates.no": "No updates", | ||||||
|  |   "version": "Using v{{packageVersion}} (built on {{builtAt}}, {{buildMode}})", | ||||||
|  |   "Language": "Language", | ||||||
|  |   "Region": "Region", | ||||||
|  |   "lang.auto": "(Auto) {{detected}}", | ||||||
|  |   "region.auto": "(Auto) {{detected}}", | ||||||
|  |   "region.en_GB": "Great Britan (English)", | ||||||
|  |   "region.en_US": "United States (English)", | ||||||
|  |   "region.zh_CN": "China Mainland (Chinese)", | ||||||
|  |   "datefmt": "yyyy/MM/dd", | ||||||
|  |   "Sign out": "Sign out", | ||||||
|  |   "Notifications": "Notifications", | ||||||
|  | 
 | ||||||
|  |   "Choose Language": "Choose Language", | ||||||
|  |   "Supported": "Supported", | ||||||
|  |   "Unsupported": "Unsupported", | ||||||
|  |   "Choose Region": "Choose Region" | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								src/settings/i18n/lang-names.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/settings/i18n/lang-names.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | { | ||||||
|  |   "lang.zh-Hans": "中文(简体)", | ||||||
|  |   "lang.en": "English" | ||||||
|  | } | ||||||
							
								
								
									
										30
									
								
								src/settings/i18n/zh-Hans.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/settings/i18n/zh-Hans.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | ||||||
|  | { | ||||||
|  |   "Settings": "设置", | ||||||
|  |   "Accounts": "所有账户", | ||||||
|  |   "All Notifications": "所有通知", | ||||||
|  |   "Sign in...": "登录新账户...", | ||||||
|  |   "Reading": "阅读", | ||||||
|  |   "Prefetch Toots": "提前下载嘟文", | ||||||
|  |   "Prefetch Toots.2nd": "图图会在你可能需要的时候提前下载嘟文。", | ||||||
|  |   "This Application": "本应用", | ||||||
|  |   "About Tutu": "关于图图", | ||||||
|  |   "About Tutu.2nd": "舒服地刷嘟。", | ||||||
|  |   "updates.ready": "更新已准备好,下次开启会启动新版本", | ||||||
|  |   "updates.no": "已是最新版本", | ||||||
|  |   "version": "正在使用 v{{packageVersion}} ({{builtAt}}构建, {{buildMode}})", | ||||||
|  |   "Language": "语言", | ||||||
|  |   "Region": "区域", | ||||||
|  |   "lang.auto": "(自动){{detected}}", | ||||||
|  |   "region.auto": "(自动){{detected}}", | ||||||
|  |   "region.en_GB": "英国和苏格兰(英语)", | ||||||
|  |   "region.en_US": "美国(英语)", | ||||||
|  |   "region.zh_CN": "中国大陆(中文)", | ||||||
|  |   "datefmt": "yyyy年MM月dd日", | ||||||
|  |   "Sign out": "登出此账户", | ||||||
|  |   "Notifications": "通知", | ||||||
|  | 
 | ||||||
|  |   "Choose Language": "选择语言", | ||||||
|  |   "Supported": "已支持", | ||||||
|  |   "Unsupported": "尚未支持", | ||||||
|  |   "Choose Region": "选择区域" | ||||||
|  | } | ||||||
|  | @ -3,6 +3,8 @@ import { persistentMap } from "@nanostores/persistent"; | ||||||
| type Settings = { | type Settings = { | ||||||
|   onGoingOAuth2Process?: string; |   onGoingOAuth2Process?: string; | ||||||
|   prefetchTootsDisabled?: boolean; |   prefetchTootsDisabled?: boolean; | ||||||
|  |   language?: string; | ||||||
|  |   region?: string; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const $settings = persistentMap<Settings>( | export const $settings = persistentMap<Settings>( | ||||||
|  |  | ||||||
|  | @ -10,7 +10,8 @@ import { | ||||||
|   children, |   children, | ||||||
|   Suspense, |   Suspense, | ||||||
|   Match, |   Match, | ||||||
|   Switch as JsSwitch |   Switch as JsSwitch, | ||||||
|  |   ErrorBoundary | ||||||
| } from "solid-js"; | } from "solid-js"; | ||||||
| import { useDocumentTitle } from "../utils"; | import { useDocumentTitle } from "../utils"; | ||||||
| import { type mastodon } from "masto"; | import { type mastodon } from "masto"; | ||||||
|  | @ -125,7 +126,9 @@ const TimelinePanel: Component<{ | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <ErrorBoundary fallback={(err, reset) => { | ||||||
|  |       return <p>Oops: {String(err)}</p> | ||||||
|  |     }}> | ||||||
|       <PullDownToRefresh |       <PullDownToRefresh | ||||||
|         linkedElement={scrollLinked()} |         linkedElement={scrollLinked()} | ||||||
|         loading={snapshot.loading} |         loading={snapshot.loading} | ||||||
|  | @ -202,7 +205,7 @@ const TimelinePanel: Component<{ | ||||||
|           </Match> |           </Match> | ||||||
|         </JsSwitch> |         </JsSwitch> | ||||||
|       </div> |       </div> | ||||||
|     </> |     </ErrorBoundary> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -36,6 +36,7 @@ import Button from "../material/Button.js"; | ||||||
| import MediaAttachmentGrid from "./MediaAttachmentGrid.js"; | import MediaAttachmentGrid from "./MediaAttachmentGrid.js"; | ||||||
| import { FastAverageColor } from "fast-average-color"; | import { FastAverageColor } from "fast-average-color"; | ||||||
| import Color from "colorjs.io"; | import Color from "colorjs.io"; | ||||||
|  | import { useDateFnLocale } from "../platform/i18n"; | ||||||
| 
 | 
 | ||||||
| type TootContentViewProps = { | type TootContentViewProps = { | ||||||
|   source?: string; |   source?: string; | ||||||
|  | @ -170,6 +171,7 @@ function TootActionGroup<T extends mastodon.v1.Status>( | ||||||
| 
 | 
 | ||||||
| function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) { | function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) { | ||||||
|   const toot = () => props.status; |   const toot = () => props.status; | ||||||
|  |   const dateFnLocale = useDateFnLocale() | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div class={tootStyle.tootAuthorGrp}> |     <div class={tootStyle.tootAuthorGrp}> | ||||||
|  | @ -187,7 +189,7 @@ function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) { | ||||||
|           }} |           }} | ||||||
|         /> |         /> | ||||||
|         <time datetime={toot().createdAt}> |         <time datetime={toot().createdAt}> | ||||||
|           {formatRelative(toot().createdAt, props.now)} |           {formatRelative(toot().createdAt, props.now, {locale: dateFnLocale()})} | ||||||
|         </time> |         </time> | ||||||
|         <span> |         <span> | ||||||
|           @{toot().account.username}@{new URL(toot().account.url).hostname} |           @{toot().account.username}@{new URL(toot().account.url).hostname} | ||||||
|  |  | ||||||
|  | @ -11,5 +11,6 @@ | ||||||
|     "types": ["vite/client", "vite-plugin-pwa/solid"], |     "types": ["vite/client", "vite-plugin-pwa/solid"], | ||||||
|     "noEmit": true, |     "noEmit": true, | ||||||
|     "isolatedModules": true, |     "isolatedModules": true, | ||||||
|  |     "resolveJsonModule": true, | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue