State of work before Duo-style onboarding pivot. Includes work that will be partly reverted in the next commit (see refactor follow-up). Onboarding (will be partly reverted): - Custom Tooltip+Glow spotlight (components/OnboardingHint.tsx) - Spotlight wiring in app/profile/edit.tsx (nickname-input glow + step-progress header, onSubmitEditing auto-save, save-handler routes to /(app)/blocker) - Spotlight wiring in app/(app)/blocker.tsx (URL-filter LayerSwitchCard wrapped + auto-PATCH step='done' when filter activates) - Routing-gate branches in (app)/_layout.tsx (welcome → /onboarding/welcome, nickname → /profile/edit) - Debug-Reset-Toggle in /debug (welcome|nickname|block|done buttons + redirect) Will stay (reused in Duo flow): - Welcome-Screen app/onboarding/welcome.tsx (will become Slide 1) - Avatar-fix in profile/edit (Dicebear seed stays stable while typing) i18n + RTL: - Arabic locale (locales/ar.json, full translation incl. onboarding keys) - I18nManager.allowRTL(true) + applyRTL helper in stores/language.ts - Language-Picker option for العربية in settings - New keys: onboarding.welcome.*, step_progress, nickname_spotlight.*, block_spotlight.*, permission_denied.*, language.*, rtl_restart.* (de/en/fr/ar) NEFilter Permission Recovery (iOS): - Swift resetUrlFilter() — removeFromPreferences + fresh saveToPreferences to bypass iOS's cached denied-state (NEFilterErrorDomain code 5) - TS module def + lib/protection.ts wrapper - components/PermissionDeniedSheet.tsx — branded recovery sheet with retry + app-settings:// deep-link + fallback hint - Wired in (app)/blocker.tsx handleActivateUrlFilter (code-5 detection) Misc: - Bug fix in onboarding/welcome.tsx: apiFetch body was double-stringified (sent as JSON string instead of object → 400 invalid_step) - Bug fix in profile/edit.tsx: avatar preview Dicebear seed switched from live nickname (changed every keystroke) to stable me?.nickname Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
64 lines
1.8 KiB
TypeScript
64 lines
1.8 KiB
TypeScript
import { Alert } from 'react-native';
|
|
import { I18nManager } from 'react-native';
|
|
import { create } from 'zustand';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import i18n from '../lib/i18n';
|
|
|
|
export type AppLanguage = 'de' | 'en' | 'fr' | 'ar';
|
|
|
|
const STORAGE_KEY = '@rebreak/language';
|
|
|
|
type LanguageState = {
|
|
language: AppLanguage;
|
|
setLanguage: (lang: AppLanguage) => Promise<void>;
|
|
init: () => Promise<void>;
|
|
};
|
|
|
|
function applyRTL(lang: AppLanguage) {
|
|
const isRTL = lang === 'ar';
|
|
if (I18nManager.isRTL !== isRTL) {
|
|
I18nManager.forceRTL(isRTL);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export const useLanguageStore = create<LanguageState>((set) => ({
|
|
language: 'en',
|
|
|
|
init: async () => {
|
|
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
|
if (stored === 'de' || stored === 'en' || stored === 'fr' || stored === 'ar') {
|
|
applyRTL(stored);
|
|
await i18n.changeLanguage(stored);
|
|
set({ language: stored });
|
|
} else {
|
|
// Kein expliziter Wert gespeichert — i18n.ts hat bereits via deviceLocale
|
|
// initialisiert (Localization.getLocales()). NICHT auf 'en' overriden.
|
|
const detected =
|
|
i18n.language === 'de'
|
|
? 'de'
|
|
: i18n.language === 'fr'
|
|
? 'fr'
|
|
: i18n.language === 'ar'
|
|
? 'ar'
|
|
: 'en';
|
|
applyRTL(detected as AppLanguage);
|
|
set({ language: detected as AppLanguage });
|
|
}
|
|
},
|
|
|
|
setLanguage: async (lang) => {
|
|
await AsyncStorage.setItem(STORAGE_KEY, lang);
|
|
await i18n.changeLanguage(lang);
|
|
set({ language: lang });
|
|
const needsReload = applyRTL(lang);
|
|
if (needsReload) {
|
|
Alert.alert(
|
|
i18n.t('settings.rtl_restart_title'),
|
|
i18n.t('settings.rtl_restart_body'),
|
|
);
|
|
}
|
|
},
|
|
}));
|