mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 19:14:03 +00:00
feat(bot): persist locale preferences across mini app and replies
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
fetchMiniAppPendingMembers,
|
||||
fetchMiniAppSession,
|
||||
joinMiniAppHousehold,
|
||||
updateMiniAppLocalePreference,
|
||||
type MiniAppDashboard,
|
||||
type MiniAppPendingMember
|
||||
} from './miniapp-api'
|
||||
@@ -36,6 +37,8 @@ type SessionState =
|
||||
member: {
|
||||
displayName: string
|
||||
isAdmin: boolean
|
||||
preferredLocale: Locale | null
|
||||
householdDefaultLocale: Locale
|
||||
}
|
||||
telegramUser: {
|
||||
firstName: string | null
|
||||
@@ -51,7 +54,9 @@ const demoSession: Extract<SessionState, { status: 'ready' }> = {
|
||||
mode: 'demo',
|
||||
member: {
|
||||
displayName: 'Demo Resident',
|
||||
isAdmin: false
|
||||
isAdmin: false,
|
||||
preferredLocale: 'en',
|
||||
householdDefaultLocale: 'en'
|
||||
},
|
||||
telegramUser: {
|
||||
firstName: 'Demo',
|
||||
@@ -112,6 +117,8 @@ function App() {
|
||||
const [pendingMembers, setPendingMembers] = createSignal<readonly MiniAppPendingMember[]>([])
|
||||
const [joining, setJoining] = createSignal(false)
|
||||
const [approvingTelegramUserId, setApprovingTelegramUserId] = createSignal<string | null>(null)
|
||||
const [savingMemberLocale, setSavingMemberLocale] = createSignal(false)
|
||||
const [savingHouseholdLocale, setSavingHouseholdLocale] = createSignal(false)
|
||||
|
||||
const copy = createMemo(() => dictionary[locale()])
|
||||
const onboardingSession = createMemo(() => {
|
||||
@@ -153,7 +160,8 @@ function App() {
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
setLocale(detectLocale())
|
||||
const fallbackLocale = detectLocale()
|
||||
setLocale(fallbackLocale)
|
||||
|
||||
webApp?.ready?.()
|
||||
webApp?.expand?.()
|
||||
@@ -175,6 +183,10 @@ function App() {
|
||||
try {
|
||||
const payload = await fetchMiniAppSession(initData, joinContext().joinToken)
|
||||
if (!payload.authorized || !payload.member || !payload.telegramUser) {
|
||||
setLocale(
|
||||
payload.onboarding?.householdDefaultLocale ??
|
||||
((payload.telegramUser?.languageCode ?? fallbackLocale).startsWith('ru') ? 'ru' : 'en')
|
||||
)
|
||||
setSession({
|
||||
status: 'onboarding',
|
||||
mode: payload.onboarding?.status ?? 'open_from_group',
|
||||
@@ -192,6 +204,7 @@ function App() {
|
||||
return
|
||||
}
|
||||
|
||||
setLocale(payload.member.preferredLocale ?? payload.member.householdDefaultLocale)
|
||||
setSession({
|
||||
status: 'ready',
|
||||
mode: 'live',
|
||||
@@ -284,6 +297,7 @@ function App() {
|
||||
try {
|
||||
const payload = await joinMiniAppHousehold(initData, joinToken)
|
||||
if (payload.authorized && payload.member && payload.telegramUser) {
|
||||
setLocale(payload.member.preferredLocale ?? payload.member.householdDefaultLocale)
|
||||
setSession({
|
||||
status: 'ready',
|
||||
mode: 'live',
|
||||
@@ -297,6 +311,10 @@ function App() {
|
||||
return
|
||||
}
|
||||
|
||||
setLocale(
|
||||
payload.onboarding?.householdDefaultLocale ??
|
||||
((payload.telegramUser?.languageCode ?? locale()).startsWith('ru') ? 'ru' : 'en')
|
||||
)
|
||||
setSession({
|
||||
status: 'onboarding',
|
||||
mode: payload.onboarding?.status ?? 'pending',
|
||||
@@ -339,6 +357,71 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMemberLocaleChange(nextLocale: Locale) {
|
||||
const initData = webApp?.initData?.trim()
|
||||
const currentReady = readySession()
|
||||
|
||||
setLocale(nextLocale)
|
||||
|
||||
if (!initData || currentReady?.mode !== 'live') {
|
||||
return
|
||||
}
|
||||
|
||||
setSavingMemberLocale(true)
|
||||
|
||||
try {
|
||||
const updated = await updateMiniAppLocalePreference(initData, nextLocale, 'member')
|
||||
|
||||
setSession((current) =>
|
||||
current.status === 'ready'
|
||||
? {
|
||||
...current,
|
||||
member: {
|
||||
...current.member,
|
||||
preferredLocale: updated.memberPreferredLocale,
|
||||
householdDefaultLocale: updated.householdDefaultLocale
|
||||
}
|
||||
}
|
||||
: current
|
||||
)
|
||||
setLocale(updated.effectiveLocale)
|
||||
} finally {
|
||||
setSavingMemberLocale(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleHouseholdLocaleChange(nextLocale: Locale) {
|
||||
const initData = webApp?.initData?.trim()
|
||||
const currentReady = readySession()
|
||||
if (!initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin) {
|
||||
return
|
||||
}
|
||||
|
||||
setSavingHouseholdLocale(true)
|
||||
|
||||
try {
|
||||
const updated = await updateMiniAppLocalePreference(initData, nextLocale, 'household')
|
||||
|
||||
setSession((current) =>
|
||||
current.status === 'ready'
|
||||
? {
|
||||
...current,
|
||||
member: {
|
||||
...current.member,
|
||||
householdDefaultLocale: updated.householdDefaultLocale
|
||||
}
|
||||
}
|
||||
: current
|
||||
)
|
||||
|
||||
if (!currentReady.member.preferredLocale) {
|
||||
setLocale(updated.effectiveLocale)
|
||||
}
|
||||
} finally {
|
||||
setSavingHouseholdLocale(false)
|
||||
}
|
||||
}
|
||||
|
||||
const renderPanel = () => {
|
||||
switch (activeNav()) {
|
||||
case 'balances':
|
||||
@@ -396,6 +479,34 @@ function App() {
|
||||
case 'house':
|
||||
return readySession()?.member.isAdmin ? (
|
||||
<div class="balance-list">
|
||||
<article class="balance-item">
|
||||
<header>
|
||||
<strong>{copy().householdLanguage}</strong>
|
||||
<span>{readySession()?.member.householdDefaultLocale.toUpperCase()}</span>
|
||||
</header>
|
||||
<div class="locale-switch__buttons">
|
||||
<button
|
||||
classList={{
|
||||
'is-active': readySession()?.member.householdDefaultLocale === 'en'
|
||||
}}
|
||||
type="button"
|
||||
disabled={savingHouseholdLocale()}
|
||||
onClick={() => void handleHouseholdLocaleChange('en')}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
'is-active': readySession()?.member.householdDefaultLocale === 'ru'
|
||||
}}
|
||||
type="button"
|
||||
disabled={savingHouseholdLocale()}
|
||||
onClick={() => void handleHouseholdLocaleChange('ru')}
|
||||
>
|
||||
RU
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
<article class="balance-item">
|
||||
<header>
|
||||
<strong>{copy().pendingMembersTitle}</strong>
|
||||
@@ -470,14 +581,16 @@ function App() {
|
||||
<button
|
||||
classList={{ 'is-active': locale() === 'en' }}
|
||||
type="button"
|
||||
onClick={() => setLocale('en')}
|
||||
disabled={savingMemberLocale()}
|
||||
onClick={() => void handleMemberLocaleChange('en')}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
<button
|
||||
classList={{ 'is-active': locale() === 'ru' }}
|
||||
type="button"
|
||||
onClick={() => setLocale('ru')}
|
||||
disabled={savingMemberLocale()}
|
||||
onClick={() => void handleMemberLocaleChange('ru')}
|
||||
>
|
||||
RU
|
||||
</button>
|
||||
|
||||
@@ -27,6 +27,8 @@ export const dictionary = {
|
||||
'The real session gate needs Telegram mini app data. Local development falls back to a preview shell.',
|
||||
reload: 'Retry',
|
||||
language: 'Language',
|
||||
householdLanguage: 'Household language',
|
||||
savingLanguage: 'Saving…',
|
||||
home: 'Home',
|
||||
balances: 'Balances',
|
||||
ledger: 'Ledger',
|
||||
@@ -90,6 +92,8 @@ export const dictionary = {
|
||||
'Настоящая проверка требует данные Telegram Mini App. Локально показывается демо-оболочка.',
|
||||
reload: 'Повторить',
|
||||
language: 'Язык',
|
||||
householdLanguage: 'Язык дома',
|
||||
savingLanguage: 'Сохраняем…',
|
||||
home: 'Главная',
|
||||
balances: 'Баланс',
|
||||
ledger: 'Леджер',
|
||||
|
||||
@@ -5,6 +5,8 @@ export interface MiniAppSession {
|
||||
member?: {
|
||||
displayName: string
|
||||
isAdmin: boolean
|
||||
preferredLocale: 'en' | 'ru' | null
|
||||
householdDefaultLocale: 'en' | 'ru'
|
||||
}
|
||||
telegramUser?: {
|
||||
firstName: string | null
|
||||
@@ -14,9 +16,17 @@ export interface MiniAppSession {
|
||||
onboarding?: {
|
||||
status: 'join_required' | 'pending' | 'open_from_group'
|
||||
householdName?: string
|
||||
householdDefaultLocale?: 'en' | 'ru'
|
||||
}
|
||||
}
|
||||
|
||||
export interface MiniAppLocalePreference {
|
||||
scope: 'member' | 'household'
|
||||
effectiveLocale: 'en' | 'ru'
|
||||
memberPreferredLocale: 'en' | 'ru' | null
|
||||
householdDefaultLocale: 'en' | 'ru'
|
||||
}
|
||||
|
||||
export interface MiniAppPendingMember {
|
||||
telegramUserId: string
|
||||
displayName: string
|
||||
@@ -219,3 +229,34 @@ export async function approveMiniAppPendingMember(
|
||||
throw new Error(payload.error ?? 'Failed to approve member')
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateMiniAppLocalePreference(
|
||||
initData: string,
|
||||
locale: 'en' | 'ru',
|
||||
scope: 'member' | 'household'
|
||||
): Promise<MiniAppLocalePreference> {
|
||||
const response = await fetch(`${apiBaseUrl()}/api/miniapp/preferences/locale`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
initData,
|
||||
locale,
|
||||
scope
|
||||
})
|
||||
})
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean
|
||||
authorized?: boolean
|
||||
locale?: MiniAppLocalePreference
|
||||
error?: string
|
||||
}
|
||||
|
||||
if (!response.ok || !payload.authorized || !payload.locale) {
|
||||
throw new Error(payload.error ?? 'Failed to update locale preference')
|
||||
}
|
||||
|
||||
return payload.locale
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user