feat: localstorage与SecureStore

This commit is contained in:
jinyaqiu 2025-06-23 11:23:31 +08:00
parent 6ab9f5db6a
commit 9939fd23aa
5 changed files with 88 additions and 19 deletions

View File

@ -34,6 +34,13 @@
"resizeMode": "contain",
"backgroundColor": "#ffffff"
}
],
[
"expo-secure-store",
{
"configureAndroidBackup": true,
"faceIDPermission": "Allow $(PRODUCT_NAME) to access your Face ID biometric data."
}
]
],
"experiments": {

View File

@ -1,35 +1,68 @@
import React, { createContext, useState, useContext, useEffect } from 'react';
import * as SecureStore from 'expo-secure-store';
import { Platform } from 'react-native';
import React, { createContext, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
type LanguageContextType = {
currentLanguage: string;
changeLanguage: (lang: string) => void;
};
// 保存语言设置
async function save(key: string, value: string) {
if (Platform.OS === 'web') {
localStorage.setItem(key, value);
} else {
await SecureStore.setItemAsync(key, value);
}
}
// 获取语言设置
async function load(key: string): Promise<string | null> {
if (Platform.OS === 'web') {
return localStorage.getItem(key);
} else {
return await SecureStore.getItemAsync(key);
}
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { i18n } = useTranslation();
// 从localStorage获取保存的语言设置如果没有则使用当前语言或默认为'en'
const savedLanguage = localStorage.getItem('i18nextLng') || i18n.language || 'en';
const [currentLanguage, setCurrentLanguage] = useState(savedLanguage);
const [currentLanguage, setCurrentLanguage] = useState(i18n.language || 'en');
// 初始化时加载保存的语言设置
useEffect(() => {
const loadSavedLanguage = async () => {
try {
const savedLang = await load('i18nextLng');
if (savedLang) {
setCurrentLanguage(savedLang);
i18n.changeLanguage(savedLang);
}
} catch (error) {
console.error('Failed to load language setting:', error);
}
};
loadSavedLanguage();
}, [i18n]);
const changeLanguage = (lang: string) => {
i18n.changeLanguage(lang);
// 将语言设置保存到localStorage
localStorage.setItem('i18nextLng', lang);
// 保存语言设置
save('i18nextLng', lang).catch(error => {
console.error('Failed to save language setting:', error);
});
};
useEffect(() => {
// 确保初始化时使用正确的语言
if (savedLanguage && i18n.language !== savedLanguage) {
i18n.changeLanguage(savedLanguage);
}
const handleLanguageChanged = () => {
setCurrentLanguage(i18n.language);
// 将更改后的语言保存到localStorage
localStorage.setItem('i18nextLng', i18n.language);
const newLanguage = i18n.language;
setCurrentLanguage(newLanguage);
// 将更改后的语言保存到存储
save('i18nextLng', newLanguage).catch(error => {
console.error('Failed to save language setting:', error);
});
};
i18n.on('languageChanged', handleLanguageChanged);
@ -37,7 +70,7 @@ export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ chil
return () => {
i18n.off('languageChanged', handleLanguageChanged);
};
}, [i18n, savedLanguage]);
}, [i18n]);
return (
<LanguageContext.Provider value={{ currentLanguage, changeLanguage }}>

View File

@ -1,6 +1,7 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import { Platform } from 'react-native';
// 自动生成的导入
import translations from './translations-generated';
@ -28,13 +29,30 @@ i18n
escapeValue: false, // 不需要转义React已经处理了
},
detection: {
detection: Platform.OS === 'web' ? {
// Web 环境使用 localStorage
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
lookupLocalStorage: 'i18nextLng',
// 确保语言设置被保存到localStorage
lookupFromPathIndex: 0,
lookupFromSubdomainIndex: 0
} : {
// 原生环境使用自定义存储
order: ['customStorage', 'device'],
caches: ['customStorage'],
// 使用类型断言解决类型问题
...({
lookupCustomStorage: {
getItem: async (key: string) => {
const { getItemAsync } = await import('expo-secure-store');
return getItemAsync(key);
},
setItem: async (key: string, value: string) => {
const { setItemAsync } = await import('expo-secure-store');
await setItemAsync(key, value);
}
}
} as any) // 使用类型断言绕过类型检查
}
});

10
package-lock.json generated
View File

@ -23,6 +23,7 @@
"expo-linking": "~7.1.5",
"expo-localization": "^16.1.5",
"expo-router": "~5.1.0",
"expo-secure-store": "~14.2.3",
"expo-splash-screen": "~0.30.9",
"expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.5",
@ -7262,6 +7263,15 @@
"node": ">=10"
}
},
"node_modules/expo-secure-store": {
"version": "14.2.3",
"resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-14.2.3.tgz",
"integrity": "sha512-hYBbaAD70asKTFd/eZBKVu+9RTo9OSTMMLqXtzDF8ndUGjpc6tmRCoZtrMHlUo7qLtwL5jm+vpYVBWI8hxh/1Q==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-splash-screen": {
"version": "0.30.9",
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.30.9.tgz",

View File

@ -46,7 +46,8 @@
"react-native-safe-area-context": "^5.4.0",
"react-native-screens": "~4.11.1",
"react-native-web": "~0.20.0",
"react-native-webview": "13.13.5"
"react-native-webview": "13.13.5",
"expo-secure-store": "~14.2.3"
},
"devDependencies": {
"@babel/core": "^7.25.2",