如何在Android中全局拦截资源加载?基于Lokalise翻译SDK的实践问题求解
Hey there, let's tackle your Lokalise SDK global adaptation issues step by step. I've gone through your problem details and here's how to solve each of your core questions:
1. ContextThemeWrapper技巧:可行,关键细节补全
Yes, that approach is totally workable. The missing piece is the cloneInContext implementation for your custom LayoutInflater, plus a properly implemented CustomResources that delegates to Lokalise's resources. Here's the complete code:
Step 1: Fix CustomLayoutInflater's cloneInContext
LayoutInflater relies on cloneInContext to create new instances when needed (like for nested layouts). You need to return your custom inflater here:
class CustomLayoutInflater( original: LayoutInflater, newContext: Context ) : LayoutInflater(original, newContext) { override fun cloneInContext(newContext: Context): LayoutInflater { // Return a new instance of your custom inflater with the new context return CustomLayoutInflater(this, newContext) } override fun onCreateView(name: String, attrs: AttributeSet): View? { try { val view = createView(name, "android.widget.", attrs) if (view is TextView) { return overrideTextView(view, attrs) } } catch (e: ClassNotFoundException) { // Handle missing class } catch (e: InflateException) { // Handle inflate errors } return super.onCreateView(name, attrs) } private fun overrideTextView(view: TextView, attrs: AttributeSet?): TextView { val typedArray = view.context.obtainStyledAttributes(attrs, intArrayOf(android.R.attr.text)) val stringResId = typedArray.getResourceId(0, -1) if (stringResId != -1) { // Fetch text from Lokalise instead of local resources view.text = view.resources.getText(stringResId) } typedArray.recycle() return view } }
Step 2: Implement CustomResources for Lokalise
Create a custom Resources class that prioritizes Lokalise's resources:
class CustomResources( baseResources: Resources, private val lokaliseResources: LokaliseResources ) : Resources(baseResources.assets, baseResources.displayMetrics, baseResources.configuration) { override fun getString(id: Int): String { return lokaliseResources.getString(id) ?: super.getString(id) } override fun getText(id: Int): CharSequence { return lokaliseResources.getText(id) ?: super.getText(id) } override fun getStringArray(id: Int): Array<String> { return lokaliseResources.getStringArray(id) ?: super.getStringArray(id) } // Add overrides for other resource types you use (e.g., getQuantityString) }
Step 3: Update ContextWrapper to use LokaliseResources
Modify your wrapper to hold a LokaliseResources instance instead of a static string map:
class LokaliseContextWrapper( base: Context, private val lokaliseResources: LokaliseResources ) : ContextWrapper(base) { override fun getResources(): Resources { return CustomResources(super.getResources(), lokaliseResources) } override fun getSystemService(name: String): Any? { if (Context.LAYOUT_INFLATER_SERVICE == name) { return CustomLayoutInflater(LayoutInflater.from(baseContext), this) } return super.getSystemService(name) } companion object { // Helper method to wrap any context with Lokalise support fun wrap(context: Context, lokaliseResources: LokaliseResources): Context { return LokaliseContextWrapper(context, lokaliseResources) } } }
Usage for floating windows
When creating your system alert window, use the wrapped context instead of the raw Application context:
val lokaliseResources = TranslationsManager.resources!! val wrappedContext = LokaliseContextWrapper.wrap(applicationContext, lokaliseResources) val inflater = LayoutInflater.from(wrappedContext) val floatingView = inflater.inflate(R.layout.your_floating_layout, null) // Proceed to attach floatingView to the window manager
2. Correctly using ViewPump/Philology for global interception
Your earlier Philology setup failed because you weren't using ViewPump's LayoutInflater consistently, and the repository wasn't mapping resource IDs correctly. Here's the fix:
Step 1: Fix PhilologyRepository implementation
Philology uses resource names (not IDs) as keys, so you need to convert the name to an ID to fetch from Lokalise:
object MyPhilologyRepositoryFactory : PhilologyRepositoryFactory { private lateinit var lokaliseResources: LokaliseResources private lateinit var appResources: Resources // Initialize with required resources fun init(appContext: Context, lokaliseRes: LokaliseResources) { appResources = appContext.resources lokaliseResources = lokaliseRes } override fun getPhilologyRepository(locale: Locale): PhilologyRepository { return object : PhilologyRepository { override fun getText(key: String): CharSequence? { // Convert resource name to ID val resId = appResources.getIdentifier(key, "string", appResources.getResourcePackageName(0)) return if (resId != 0) lokaliseResources.getText(resId) else super.getText(key) } override fun getPlural(key: String, quantityString: String): CharSequence? { val resId = appResources.getIdentifier(key, "string", appResources.getResourcePackageName(0)) val quantity = quantityString.toIntOrNull() ?: return super.getPlural(key, quantityString) return if (resId != 0) lokaliseResources.getQuantityString(resId, quantity) else super.getPlural(key, quantityString) } override fun getTextArray(key: String): Array<CharSequence>? { val resId = appResources.getIdentifier(key, "array", appResources.getResourcePackageName(0)) return if (resId != 0) lokaliseResources.getTextArray(resId) else super.getTextArray(key) } } } }
Step 2: Properly initialize ViewPump in your Application
Make sure to initialize ViewPump and Philology in the right order, and override the Application's LayoutInflater to use ViewPump globally:
class MyApp : Application() { override fun onCreate() { super.onCreate() // 1. Initialize Lokalise first Lokalise.init(this, Keys.LOCALISE_SDK_TOKEN, Keys.LOCALISE_PROJECT_ID) Lokalise.updateTranslations() val lokaliseResources = LokaliseResources(this) TranslationsManager.initOnAppOnCreate(this) // Update your manager to hold the instance // 2. Initialize Philology with our factory MyPhilologyRepositoryFactory.init(this, lokaliseResources) Philology.init(MyPhilologyRepositoryFactory) // 3. Initialize ViewPump with Philology interceptor ViewPump.init(ViewPump.builder() .addInterceptor(PhilologyInterceptor) .build()) } // Override to return ViewPump's LayoutInflater globally override fun getSystemService(name: String): Any? { return if (Context.LAYOUT_INFLATER_SERVICE == name) { ViewPumpLayoutInflater.from(this) } else { super.getSystemService(name) } } }
Step 3: Use ViewPump's LayoutInflater everywhere
For custom views and floating windows, explicitly use ViewPumpLayoutInflater.from(context) instead of the default inflater. This ensures the Philology interceptor catches all text resource loads.
3. Global resource loading without per-class modifications
Yes, you have two solid options here:
Option A: Override Application's Resources and LayoutInflater
Modify your Application class to return your custom Resources and LayoutInflater for all context requests. This covers every scenario that uses the Application context (including floating windows):
class MyApp : Application() { private lateinit var lokaliseResources: LokaliseResources override fun onCreate() { super.onCreate() // Initialize Lokalise first Lokalise.init(this, Keys.LOCALISE_SDK_TOKEN, Keys.LOCALISE_PROJECT_ID) Lokalise.updateTranslations() lokaliseResources = LokaliseResources(this) } override fun getResources(): Resources { // Only return custom resources if Lokalise is initialized return if (::lokaliseResources.isInitialized) { CustomResources(super.getResources(), lokaliseResources) } else { super.getResources() } } override fun getSystemService(name: String): Any? { return if (Context.LAYOUT_INFLATER_SERVICE == name) { CustomLayoutInflater(LayoutInflater.from(baseContext), this) } else { super.getSystemService(name) } } }
This way, every call to context.resources or LayoutInflater.from(context) will use your Lokalise-backed resources and inflater, no per-class changes needed.
Option B: Android 30+ ResourcesProvider (Official Solution)
For devices running Android 11 (API 30+) and above, use the official ResourcesProvider to inject Lokalise resources into the global resource system:
class MyApp : Application() { override fun onCreate() { super.onCreate() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { Lokalise.init(this, Keys.LOCALISE_SDK_TOKEN, Keys.LOCALISE_PROJECT_ID) Lokalise.updateTranslations() val lokaliseResources = LokaliseResources(this) // Create a provider that delegates to Lokalise val lokaliseProvider = ResourcesProvider().apply { addResourceProvider(object : ResourcesProvider() { override fun getString(resId: Int): String? { return lokaliseResources.getString(resId) } override fun getText(resId: Int): CharSequence? { return lokaliseResources.getText(resId) } // Add other resource type overrides as needed }) } // Add the provider to the global resources resources.addResourcesProvider(lokaliseProvider) } else { // Fall back to the Application override method for older devices // ... (implement Option A logic here) } } }
This is the cleanest approach for modern Android versions, as it integrates directly with the system's resource loading pipeline.
4. Coverage of all resource loading scenarios
The above solutions cover all common scenarios:
- ✅ Activity/Service: Handled via Application-level overrides or
attachBaseContext - ✅ Floating windows: Uses wrapped Application context or global resources
- ✅ Custom Views: Automatically uses global Resources/LayoutInflater
- ✅ Layout XML inflation: Custom LayoutInflater or ViewPump intercepts text resources
- ✅ Direct resource calls (
getResources().getString(resId)): Custom Resources or ResourcesProvider takes priority - ✅
TextView.setText(resId): Covered by Resources overrides or Philology interception
Edge cases to watch for:
- Initialization timing: Ensure Lokalise is initialized before any resource calls (do this early in
Application.onCreate()). - Language switching: After calling
Lokalise.updateTranslations(), you may need to recreate contexts or refresh resource configurations to apply changes. - System resources: Avoid overriding system resources (stick to your app's own resource IDs) to prevent unexpected behavior.
内容的提问来源于stack exchange,提问作者android developer

