diff --git a/src/core/i18n/index.ts b/src/core/i18n/index.ts index 7f8361d..c3ec1ef 100644 --- a/src/core/i18n/index.ts +++ b/src/core/i18n/index.ts @@ -1,11 +1,12 @@ +import { logger } from "@lib/utils/logger"; import { FluxDispatcher } from "@metro/common"; -import { findByNameLazy } from "@metro/wrappers"; +import { findByNameLazy, findByStoreName } from "@metro/wrappers"; import { PrimitiveType } from "intl-messageformat"; import langDefault from "./default.json"; const IntlMessageFormat = findByNameLazy( - "MessageFormat", + "MessageFormat", ) as typeof import("intl-messageformat").default; type I18nKey = keyof typeof langDefault; @@ -17,61 +18,133 @@ const _loadedLocale = new Set(); const _loadedStrings = {} as Record; export const Strings = new Proxy( - {}, - { - get: (_t, prop: keyof typeof langDefault) => { - if (_currentLocale && _loadedStrings[_currentLocale]?.[prop]) { - return _loadedStrings[_currentLocale]?.[prop]; - } - return langDefault[prop]; + {}, + { + get: (_t, prop: keyof typeof langDefault) => { + if (_currentLocale && _loadedStrings[_currentLocale]?.[prop]) { + return _loadedStrings[_currentLocale]?.[prop]; + } + return langDefault[prop]; + }, }, - }, ) as Record; -export function initFetchI18nStrings() { - const cb = ({ locale }: { locale: string }) => { - const languageMap = { - "es-ES": "es", - "es-419": "es_419", - "zh-TW": "zh-Hant", - "zh-CN": "zh-Hans", - "pt-PT": "pt", - "pt-BR": "pt_BR", - "sv-SE": "sv", - } as Record; +const languageMap: Record = { + "ar-SA": "ar", + "bn-BD": "bn", + "ca-ES": "ca", + "de-DE": "de", + "es-ES": "es", + "es-419": "es", + "fa-IR": "fa", + "fi-FI": "fi", + "fr-FR": "fr", + "hi-IN": "hi", + "hr-HR": "hr", + "hu-HU": "hu", + "id-ID": "id", + "it-IT": "it", + "ja-JP": "ja", + "pl-PL": "pl", + "pt-BR": "pt_BR", + "ru-RU": "ru", + "sk-SK": "sk", + "sv-SE": "sv", + "tr-TR": "tr", + "vi-VN": "vi", +}; +function fetchLocale(locale: string) { const resolvedLocale = (_lastSetLocale = languageMap[locale] ?? locale); - if (resolvedLocale.startsWith("en-")) { - _currentLocale = null; - return; - } + logger.log("[i18n] fetchLocale called:", locale, "->", resolvedLocale); if (!_loadedLocale.has(resolvedLocale)) { - _loadedLocale.add(resolvedLocale); + _loadedLocale.add(resolvedLocale); - fetch( - `https://raw.githubusercontent.com/kmmiio99o/i18n/main/resources/${resolvedLocale}/shiggycord.json`, - ) - .then((r) => r.json()) - .then((strings) => (_loadedStrings[resolvedLocale] = strings)) - .then( - () => - resolvedLocale === _lastSetLocale && - (_currentLocale = resolvedLocale), - ) - .catch((e) => - console.error( - `An error occured while fetching strings for ${resolvedLocale}: ${e}`, - ), - ); + if (resolvedLocale.toLowerCase().startsWith("en")) { + // use local default.json for english + logger.log("[i18n] Using local default.json for English locale"); + _loadedStrings[resolvedLocale] = langDefault; + _currentLocale = resolvedLocale; + } else { + fetch(`https://codeberg.org/cocobo1/kettu-i18n/raw/branch/main/base/${resolvedLocale}.json`) + .then(r => r.json()) + .then(strings => { + logger.log("[i18n] Loaded strings for:", resolvedLocale); + _loadedStrings[resolvedLocale] = strings; + _currentLocale = resolvedLocale; + }) + .catch(e => + logger.error(`[i18n] Error fetching strings for ${resolvedLocale}: ${e}`), + ); + } } else { - _currentLocale = resolvedLocale; + _currentLocale = resolvedLocale; } - }; +} - FluxDispatcher.subscribe("I18N_LOAD_SUCCESS", cb); - return () => FluxDispatcher.unsubscribe("I18N_LOAD_SUCCESS", cb); +export function initFetchI18nStrings() { + let attempts = 0; + const checkAndFetch = () => { + attempts++; + + try { + const LocaleStore = findByStoreName("LocaleStore"); + logger.log("[i18n] Attempt", attempts, "- LocaleStore:", !!LocaleStore); + + if (!LocaleStore) { + logger.log("[i18n] LocaleStore not found yet"); + return false; + } + + if (LocaleStore?._isInitialized !== true) { + logger.log("[i18n] LocaleStore not initialized yet"); + return false; + } + + const locale = LocaleStore.locale; + + if (locale) { + logger.log("[i18n] Using LocaleStore:", locale); + fetchLocale(locale); + return true; + } + + } catch (e) { + logger.log("[i18n] Error:", e); + } + return false; + }; + + const tryTimes = () => { + if (checkAndFetch()) return; + if (attempts < 15) { + setTimeout(tryTimes, 500); + } + }; + + tryTimes(); + + const cb = (e: any) => { + if (e?.settings?.changes?.loading) { + logger.log("[i18n] Settings loading, skipping..."); + return; + } + + const locale = e?.settings?.changes?.protoToSave?.localization?.locale?.value; + logger.log("[i18n] Locale changed:", locale); + if (locale) { + logger.log("[i18n] Found locale in event:", locale); + fetchLocale(locale); + } + }; + + FluxDispatcher.subscribe("USER_SETTINGS_PROTO_UPDATE_EDIT_INFO", cb); + + return () => { + FluxDispatcher.unsubscribe("USER_SETTINGS_PROTO_UPDATE_EDIT_INFO", cb); + }; } type FormatStringRet = T extends PrimitiveType @@ -79,10 +152,10 @@ type FormatStringRet = T extends PrimitiveType : string | T | (string | T)[]; export function formatString( - key: I18nKey, - val: Record, + key: I18nKey, + val: Record, ): FormatStringRet { - const str = Strings[key]; - // @ts-ignore - return new IntlMessageFormat(str).format(val); + const str = Strings[key]; + // @ts-ignore + return new IntlMessageFormat(str).format(val); } diff --git a/src/core/plugins/badges/index.tsx b/src/core/plugins/badges/index.tsx index 3cca7f2..bfd555a 100644 --- a/src/core/plugins/badges/index.tsx +++ b/src/core/plugins/badges/index.tsx @@ -1,15 +1,43 @@ import { after } from "@lib/api/patcher"; import { onJsxCreate } from "@lib/api/react/jsx"; -import { findByName } from "@metro"; +import { findByName, findByNameLazy } from "@metro"; import { useEffect, useState } from "react"; import { defineCorePlugin } from ".."; +import { FluxDispatcher } from "@metro/common"; -interface BunnyBadge { - label: string; - url: string; +interface Badge { + label: string; + url: string; } -const useBadgesModule = findByName("useBadges", false); +interface CustomBadge { + label: string; + url: string; +} + +interface UserBadgeData { + roles?: string[]; + custom?: CustomBadge[]; +} + +interface BadgeData { + [userId: string]: UserBadgeData; +} + +interface RoleData { + label: string; + url: string; +} + +interface RolesData { + [roleName: string]: RoleData; +} + +const useBadgesModule = findByNameLazy("useBadges", false); + +const badgesCache = new Map(); +const badgeProps = new Map>(); +const pendingRequests = new Set(); export default defineCorePlugin({ manifest: { @@ -26,69 +54,101 @@ export default defineCorePlugin({ }, start() { - let allBadges: { [x: string]: any } | null = null; - const badgeProps = {} as Record; - - onJsxCreate("ProfileBadge", (component, ret) => { - if (ret.props.id?.startsWith("bunny-")) { - const cachedProps = badgeProps[ret.props.id]; - if (cachedProps) { - ret.props.source = cachedProps.source; - ret.props.label = cachedProps.label; - ret.props.id = cachedProps.id; - } - } - }); - - onJsxCreate("RenderedBadge", (component, ret) => { - if (ret.props.id?.startsWith("bunny-")) { - const cachedProps = badgeProps[ret.props.id]; - if (cachedProps) { - Object.assign(ret.props, cachedProps); - } - } - }); - - after("default", useBadgesModule, ([user], result) => { - const [badges, setBadges] = useState([]); - - useEffect(() => { - if (!user) return; - - if (!allBadges) { - fetch( - "https://codeberg.org/raincord/badges/raw/branch/main/badges.json", - ) - .then((r) => r.json()) - .then((data) => { - allBadges = data; - //@ts-expect-error - setBadges(allBadges[user.userId] || []); - }); - } else { - //user has no badges, but maybe they should get some by contributing - setBadges(allBadges[user.userId] || []); - } - }, [user?.userId]); - - if (user && badges.length > 0) { - badges.forEach((badge, i) => { - const badgeId = `bunny-${user.userId}-${i}`; - - badgeProps[badgeId] = { - id: badgeId, - source: { uri: badge.url }, - label: badge.label, - userId: user.userId, - }; - - result.push({ - id: badgeId, - description: badge.label, - icon: " _", - }); + onJsxCreate("ProfileBadge", (component, ret) => { + if (ret.props.id?.startsWith("rain-")) { + const cachedProps = badgeProps.get(ret.props.id); + if (cachedProps) { + ret.props.source = cachedProps.source; + ret.props.label = cachedProps.label; + ret.props.id = cachedProps.id; + } + } }); - } - }); - }, + + onJsxCreate("RenderedBadge", (component, ret) => { + if (ret.props.id?.startsWith("rain-")) { + const cachedProps = badgeProps.get(ret.props.id); + if (cachedProps) { + Object.assign(ret.props, cachedProps); + } + } + }); + + const fetchAndProcessBadges = async (userId: string) => { + if (pendingRequests.has(userId)) return; + pendingRequests.add(userId); + + try { + const [badgesRes, rolesRes] = await Promise.all([ + fetch("https://codeberg.org/raincord/badges/raw/branch/main/badges.json"), + fetch("https://codeberg.org/raincord/badges/raw/branch/main/assets/roles/roles.json"), + ]); + + const badgesData: BadgeData = await badgesRes.json(); + const rolesData: RolesData = await rolesRes.json(); + + const userBadgeData = badgesData[userId] || { roles: [], custom: [] }; + + const allBadges: Badge[] = []; + + // process role badges + if (userBadgeData.roles) { + userBadgeData.roles.forEach(roleName => { + const roleData = rolesData[roleName]; + if (roleData) { + allBadges.push({ + label: roleData.label, + url: roleData.url, + }); + } + }); + } + + // process custom badges + if (userBadgeData.custom) { + allBadges.push(...userBadgeData.custom); + } + + badgesCache.set(userId, allBadges); + + allBadges.forEach((badge, i) => { + const badgeId = `rain-${userId}-${i}`; + badgeProps.set(badgeId, { + id: badgeId, + source: { uri: badge.url }, + label: badge.label, + userId, + }); + }); + + FluxDispatcher.dispatch({ type: "USER_UPDATE", user: { id: userId } }); + } finally { + pendingRequests.delete(userId); + } + }; + + after("default", useBadgesModule, ([user], result) => { + if (!user) return; + + const userId = user.userId; + const cached = badgesCache.get(userId); + + if (!cached) { + if (!pendingRequests.has(userId)) { + fetchAndProcessBadges(userId); + } + return; + } + + cached.forEach((badge, i) => { + const badgeId = `rain-${userId}-${i}`; + + result.unshift({ + id: badgeId, + description: badge.label, + icon: " _", + }); + }); + }); + }, });