Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
kmmiio99o 2026-03-21 13:08:05 +01:00
commit 63bfe21eb7
No known key found for this signature in database
GPG key ID: 163FE39F7772D2EC
2 changed files with 252 additions and 119 deletions

View file

@ -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<string>();
const _loadedStrings = {} as Record<string, typeof langDefault>;
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<I18nKey, string>;
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<string, string>;
const languageMap: Record<string, string> = {
"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> = T extends PrimitiveType
@ -79,10 +152,10 @@ type FormatStringRet<T> = T extends PrimitiveType
: string | T | (string | T)[];
export function formatString<T = void>(
key: I18nKey,
val: Record<string, T>,
key: I18nKey,
val: Record<string, T>,
): FormatStringRet<T> {
const str = Strings[key];
// @ts-ignore
return new IntlMessageFormat(str).format(val);
const str = Strings[key];
// @ts-ignore
return new IntlMessageFormat(str).format(val);
}

View file

@ -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<string, Badge[]>();
const badgeProps = new Map<string, Record<string, any>>();
const pendingRequests = new Set<string>();
export default defineCorePlugin({
manifest: {
@ -26,69 +54,101 @@ export default defineCorePlugin({
},
start() {
let allBadges: { [x: string]: any } | null = null;
const badgeProps = {} as Record<string, any>;
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<BunnyBadge[]>([]);
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: " _",
});
});
});
},
});