You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Android中全局拦截资源加载?基于Lokalise翻译SDK的实践问题求解

解决方案:全局适配Lokalise SDK到Android所有场景(含悬浮窗)

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 12:42:42