ikws4 / WeiJu2-Scripts

MIT License
26 stars 1 forks source link

[working up to Android 13] Script to change an app's locale/ language #5

Closed LeeBinder closed 7 months ago

LeeBinder commented 7 months ago

[EDIT] Verified working scripts::

Each script might also work in other versions of Android. The script for Android 11, however, does not work in Android 13!


Hi @ikws4. With WeiJu 0.3.7/ 0.3.8 it's possible to change an app's locale (to change not just its UI but also its content language, if the app's displayed content is inherently linked to its locale). That was/ is awesome! But sadly WeiJu is not compatible with Android 13+ :(

As per your email to me:

You could try the newer version of WeiJu, which has all the functionalities except the translation utility. You can upgrade from the old version, all your data will be migrated to the new script format. I hope it will solve your problems. 😃

Unfortunately, even after lots of tinkering, I have not been able to change my desired app's language into EN with the new WeiJu2: WeiJu2 activated in LSPosed, the app enabled in WeiJu's LSPosed settings (I suppose System Framework still doesn't need to be enabled), Superuser access request granted in Magisk pop-up/ toast, phone rebooted > migrate_init script successfully generated:

ojcamboijnmigibi

The unedited script causes the app to crash, but so does the script after changing language = "en_", to language = "en-US", > restart WeiJu > ▶ (tested on two different phones with two different versions of Android). At least it means WeiJu2 is operating and the script has some effect, just not what's desired:

Here's what the self-imported migrate_init script looks like - click to expand/ contract ◀ ```lua --[=[ @metadata return { name = "migrate_init", author = "ikws4", version = "1.0.0", description = "Help users migrate from older version of WeiJu (0.3.7)" } @end --]=] local config = { status_bar = { is_enable_status_bar = false, is_hide_status_bar = false, immersive_status_bar = nil, custom_status_bar_color = nil, status_bar_icon_color = nil, }, nav_bar = { is_enable_nav_bar = false, is_hide_nav_bar = false, immersive_nav_bar = nil, custom_nav_bar_color = nil, nav_bar_icon_color = nil, }, screen = { is_enable_screen = true, screen_orientation = nil, is_enable_force_screenshot = false, is_cancel_dialog = false, --language = "en_", language = "en-US", custom_dpi = nil, }, variable = { is_enable_variable = false, device = nil, product_name = nil, model = nil, brand = nil, release = nil, longitude = nil, latitude = nil, imei = nil, imsi = nil, }, } local function setup_status_bar() local status_bar = config.status_bar if not status_bar.is_enable_status_bar then return end local R = import("android.R") local Activity = import("android.app.Activity") local Color = import("android.graphics.Color") local Build = import("android.os.Build") local Bundle = import("android.os.Bundle") local TypedValue = import("android.util.TypedValue") local View = import("android.view.View") local LayoutParams = import("android.view.WindowManager.LayoutParams") local Toast = import("android.widget.Toast") hook { class = Activity, returns = void, method = "onCreate", params = { Bundle, }, after = function(this, params) local window = this:getWindow() if status_bar.immersive_status_bar then local color = 0 local immersive = status_bar.immersive_status_bar local theme = this:getTheme() local typedValue = TypedValue() if immersive == "ColorPrimary" then color = theme:resolveAttribute(R.attr.colorPrimary, typedValue, true) elseif immersive == "ColorPrimaryDark" then color = theme:resolveAttribute(R.attr.colorPrimaryDark, typedValue, true) elseif immersive == "ColorAccent" then color = theme:resolveAttribute(R.attr.colorAccent, typedValue, true) end color = typedValue.data if immersive == "Custom" then if status_bar.custom_status_bar_color then local ok, _color = pcall(Color.parseColor, Color, status_bar.custom_status_bar_color) if ok then color = _color else Toast:makeText(this, "Invalid custom status bar color", Toast.LENGTH_SHORT):show() end end end window:setStatusBarColor(color) elseif status_bar.custom_status_bar_color then window:setStatusBarColor(Color:parseColor(status_bar.custom_status_bar_color)) end local decorView = window:getDecorView() local flags = decorView:getSystemUiVisibility() if status_bar.is_hide_status_bar then window:addFlags(LayoutParams.FLAG_FULLSCREEN) else window:clearFlags(LayoutParams.FLAG_FULLSCREEN) end if Build.VERSION.SDK_INT >= Build.VERSION_CODES.M then if status_bar.status_bar_icon_color == "Grey" then decorView:setSystemUiVisibility(bit32.bor(flags, View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)) elseif status_bar.status_bar_icon_color == "White" then decorView:setSystemUiVisibility(bit32.bxor(flags, View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)) end end end, } end local function setup_nav_bar() local nav_bar = config.nav_bar if not nav_bar.is_enable_nav_bar then return end local R = import("android.R") local Activity = import("android.app.Activity") local Color = import("android.graphics.Color") local Build = import("android.os.Build") local Bundle = import("android.os.Bundle") local TypedValue = import("android.util.TypedValue") local View = import("android.view.View") local Toast = import("android.widget.Toast") hook { class = Activity, returns = void, method = "onCreate", params = { Bundle, }, after = function(this, params) local window = this:getWindow() if nav_bar.immersive_nav_bar then local color = 0 local immersive = nav_bar.immersive_nav_bar local theme = this:getTheme() local typedValue = TypedValue() if immersive == "ColorPrimary" then color = theme:resolveAttribute(R.attr.colorPrimary, typedValue, true) elseif immersive == "ColorPrimaryDark" then color = theme:resolveAttribute(R.attr.colorPrimaryDark, typedValue, true) elseif immersive == "ColorAccent" then color = theme:resolveAttribute(R.attr.colorAccent, typedValue, true) end color = typedValue.data if immersive == "Custom" then if nav_bar.custom_nav_bar_color then local ok, _color = pcall(Color.parseColor, Color, nav_bar.custom_nav_bar_color) if ok then color = _color else Toast:makeText(this, "Invalid custom nav bar color", Toast.LENGTH_SHORT):show() end end end window:setNavigationBarColor(color) elseif nav_bar.custom_nav_bar_color then window:setNavigationBarColor(Color:parseColor(nav_bar.custom_nav_bar_color)) end local decorView = window:getDecorView() local flags = decorView:getSystemUiVisibility() if nav_bar.is_hide_nav_bar then decorView:setSystemUiVisibility( bit32.bor(flags, View.SYSTEM_UI_FLAG_HIDE_NAVIGATION, View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) ) end if Build.VERSION.SDK_INT >= Build.VERSION_CODES.O then if nav_bar.nav_bar_icon_color == "Grey" then decorView:setSystemUiVisibility(bit32.bor(flags, View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)) elseif nav_bar.nav_bar_icon_color == "White" then decorView:setSystemUiVisibility(bit32.bxor(flags, View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)) end end end, } end local function setup_screen() local screen = config.screen if not screen.is_enable_screen then return end local Activity = import("android.app.Activity") local Bundle = import("android.os.Bundle") if screen.screen_orientation then hook { class = Activity, returns = void, method = "onCreate", params = { Bundle, }, after = function(this, params) this:setRequestedOrientation(screen.screen_orientation) end, } end local Window = import("android.view.Window") local WindowManager = import("android.view.WindowManager") if screen.is_enable_force_screenshot then hook { class = Window, returns = void, method = "setFlags", params = { int, int, }, before = function(this, params) local flags = params[1] local mask = params[2] if flags == WindowManager.LayoutParams.FLAG_SECURE and mask == WindowManager.LayoutParams.FLAG_SECURE then return end end, } end local Dialog = import("android.app.Dialog") if screen.is_cancel_dialog then local methods = { "setCancelable", "setCanceledOnTouchOutside" } for _, method in ipairs(methods) do hook { class = Dialog, returns = void, method = method, params = { boolean, }, before = function(this, params) params[1] = true end, } end end local ContextWrapper = import("android.content.ContextWrapper") local Configuration = import("android.content.res.Configuration") local Context = import("android.content.Context") local String = import("java.lang.String") local Locale = import("java.util.Locale") local language_country = screen.language:split("_") if screen.language then hook { class = Locale, returns = String, method = "getLanguage", replace = function(this) return language_country[1] end, } hook { class = Locale, returns = String, method = "getCountry", replace = function(this) return language_country[2] end, } end if screen.custom_dpi or screen.language then hook { class = ContextWrapper, returns = void, method = "attachBaseContext", params = { Context, }, before = function(this, params) local context = params[1] -- Make a copy of the ration, and then modify the dpi local new_configuration = Configuration(context:getResources():getConfiguration()) if screen.custom_dpi then new_configuration.densityDpi = screen.custom_dpi end -- Change the language if screen.language then local locale = Locale(language_country[1], language_country[2]) Locale:setDefault(locale) new_configuration:setLocale(locale) end params[1] = context:createConfigurationContext(new_configuration) end, } end end local function setup_variable() local variable = config.variable if not variable.is_enable_variable then return end local Build = import("android.os.Build") Build.DEVICE = variable.device or Build.DEVICE Build.PRODUCT = variable.product_name or Build.PRODUCT Build.MODEL = variable.model or Build.MODEL Build.BRAND = variable.brand or Build.BRAND Build.MANUFACTURER = variable.brand or Build.MANUFACTURER Build.VERSION.RELEASE = variable.release or Build.VERSION.RELEASE local location_classes = { "android.location.Location", -- Android "com.baidu.location.BDLocation", -- Baidu } for _, class in ipairs(location_classes) do local ok, class = pcall(import, class) if ok then if variable.longitude then hook { class = class, returns = double, method = "getLongitude", replace = function(this, params) return variable.longitude end, } end if variable.latitude then hook { class = class, returns = double, method = "getLatitude", replace = function(this, params) return variable.latitude end, } end end end local TelephonyManager = import("android.telephony.TelephonyManager") if variable.imei then hook { class = TelephonyManager, returns = String, method = "getDeviceId", replace = function(this, params) if Build.VERSION.SDK_INT >= Build.VERSION_CODES.O then return variable.imei else return variable.imei end end, } end if variable.imsi then hook { class = TelephonyManager, returns = String, method = "getSubscriberId", replace = function(this, params) return variable.imsi end, } end end setup_status_bar() setup_nav_bar() setup_screen() setup_variable() ```

Because we're only interested in setup_screen() for this example, I commented out the other lines:

--setup_status_bar()
--setup_nav_bar()
setup_screen()
--setup_variable()

but still the app crashes :(

Maybe for this example we can stick with the app and specific version I'm trying to again have in EN because it was working before with WeiJu "classic/ legacy", Gaia (com.gaiamtv) v.4.3.5 (2834). Below links to download the apk:

Other than that, feel free to choose any app of your liking - but the script should work app-independent as long as the app contains the assigned locale (in this case en-US), shouldn't it.

Simply let me know if you need any logs from me, and how to generate it/ them 👍

ikws4 commented 7 months ago
if screen.language then
  hook {
    class = Locale,
    returns = String,
    method = "getLanguage",
    replace = function(this)
      return language_country[1]
    end,
  }

  hook {
    class = Locale,
    returns = String,
    method = "getCountry",
    replace = function(this)
      return language_country[2] or ""
    end,
  }
end

if screen.custom_dpi or screen.language then
  hook {
    class = ContextWrapper,
    returns = void,
    method = "attachBaseContext",
    params = {
      Context,
    },
    before = function(this, params)
      local context = params[1]

      print("test0")
      print(context)

      -- Make a copy of the ration, and then modify the dpi
      local new_configuration = Configuration(context:getResources():getConfiguration())

      if screen.custom_dpi then
        new_configuration.densityDpi = screen.custom_dpi
      end

      -- Change the language
      if screen.language then
        local locale = Locale(language_country[1], language_country[2] or "")
        print(locale)
        Locale:setDefault(locale)
        new_configuration:setLocale(locale)
      end

      params[1] = context:createConfigurationContext(new_configuration)
    end,
  }
end

I have added some print log and null check to the languagecountry array, now this should fix the crash problem, just give the "en" a try to see if it works.

LeeBinder commented 7 months ago

YES 👍: working in LineageOS 18.1/ Android 11 💯 - well done 🥇 👍 !!

Here's the condensed script with the name accdg. to Android target:

click to expand/ contract ◀ ```lua --[=[ @metadata return { name = "change_locale_Android_11", author = "ikws4", version = "1.0.0", description = "Change an App's language | Target: Android 11" } @end --]=] local config = { language = "en_" } local function setup_screen() local ContextWrapper = import("android.content.ContextWrapper") local Configuration = import("android.content.res.Configuration") local Context = import("android.content.Context") local String = import("java.lang.String") local Locale = import("java.util.Locale") local language_country = config.language:split("_") local lang = language_country[1] local country = language_country[2] or "" hook { class = Locale, returns = String, method = "getLanguage", replace = function(this) return lang end } hook { class = Locale, returns = String, method = "getCountry", replace = function(this) return country end } hook { class = ContextWrapper, returns = void, method = "attachBaseContext", params = { Context }, before = function(this, params) local context = params[1] local new_configuration = Configuration(context:getResources():getConfiguration()) local locale = Locale(lang, country) Locale:setDefault(locale) new_configuration:setLocale(locale) params[1] = context:createConfigurationContext(new_configuration) end } end setup_screen() ```

For convenience, here the link to your script for target Android 13.

ikws4 commented 7 months ago

That's great!

Below is the minimal example. Actually, this repo already has a script that can change the app language; check out ikws4.system_variable. However, it also has the same issues, so when I have time, I'll fix it later. For now, just leave the code here for someone to reference. 😃

If you have any question, feel free to ask me here :)

--[=[
@metadata
return {
name = "change_locale",
author = "ikws4",
version = "1.0.0",
description = "Change an app's language'"
}
@end
--]=]

local config = {
  language = "en_"
}

local function setup_screen()
  local ContextWrapper = import("android.content.ContextWrapper")
  local Configuration = import("android.content.res.Configuration")
  local Context = import("android.content.Context")
  local String = import("java.lang.String")
  local Locale = import("java.util.Locale")

  local language_country = config.language:split("_")
  local lang = language_country[1]
  local country = language_country[2] or ""

  hook {
    class = Locale,
    returns = String,
    method = "getLanguage",
    replace = function(this)
      return lang
    end
  }

  hook {
    class = Locale,
    returns = String,
    method = "getCountry",
    replace = function(this)
      return country
    end
  }

  hook {
    class = ContextWrapper,
    returns = void,
    method = "attachBaseContext",
    params = {
      Context
    },
    before = function(this, params)
      local context = params[1]

      local new_configuration = Configuration(context:getResources():getConfiguration())

      local locale = Locale(lang, country)
      Locale:setDefault(locale)
      new_configuration:setLocale(locale)

      params[1] = context:createConfigurationContext(new_configuration)
    end
  }
end

setup_screen()
LeeBinder commented 7 months ago

I tried ikws4.system_variable right after migrate_init proved not working, having to find out that it's not worming, either, so yes, it would definitely help to stop others in the future from continuing to bump into walls.

For now I'm very happy that I can watch content in EN again. Oh, BTW, indeed "en_" is the correct parameter for English because even though "en-US" translates the UI, there is no content (blank). That's in contrast to the instructions in the ReadMe here, so I think it could help others to point them to the single block country codes, rather than dual-block, and only if a simple code doesn't work with some app, use the xx-YY style.

LeeBinder commented 6 months ago

@ikws4 - unfortunately after upgrading my ROM to LineageOS 20/ Android 13, changing app locale with your script above doesn't work anymore for me 🙁 My success from before was with LineageOS 18.1/ Android 11.

Yes, the app to be translated (exact same as before above) is selected in LSPosed > WeiJu. I even added System Framework > reboot - no change as expected.

For debugging, can you share your environment by answering a few questions, please?

  1. Have you tested WeiJu2 + above 'change_locale' script in Android 13 or 14: yes/ no
  2. Is it working for you: yes/no
  3. If yes: a) which type and version of Magisk are you running? Like: Magisk (official) release channel v.27.0 b) which fork and version of LSPosed are you running? Like: https://github.com/pumPCin/LSPosed/releases/tag/1.9.3-7229 c) which ROM and Android version are you running?

This would be very helpful to narrow things down. 非常感谢 👍

LeeBinder commented 6 months ago

In the meantime I also checked with crDroid (also based on LineageOS) Android 13 - same thing.

Logcat is empty.

Do you think this is an incompatibility only w/ LineageOS >= Android 13? If there is anything else I can do for debugging, feel free to let me know 💯

ikws4 commented 6 months ago

@LeeBinder It seems that on Android 13, the ContextWrapper.attachBaseContext method is not being called anymore.

After some time looking into this, I discovered a different hook point and found that we can directly change the configuration when the system configuration is passed to the setTo method.

Below is the test code that I used on my phone, utilizing Shizuku and LSPatch. You can also modify the densityDpi. There are a lot of parameters you can tweak!

local config = {
  language = "zh_",
  dpi = 200
}

local function setup_screen()
  local Configuration = import("android.content.res.Configuration")
  local Locale = import("java.util.Locale")

  local language_country = config.language:split("_")
  local lang = language_country[1]
  local country = language_country[2] or ""
  local locale = Locale(lang, country)

  hook {
    class = Configuration,
    returns = void,
    method = "setTo",
    params = { Configuration },
    after = function(this, params)
      this.densityDpi = config.dpi
      this:setLocale(locale)
    end
  }
end

setup_screen()
LeeBinder commented 6 months ago

YES 👍, you ROCK once more - confirmed working in crDroid 9.10/ Android 13:

--[=[
@metadata
return {
name = "change_locale_Android_13",
author = "ikws4",
version = "1.0.0",
description = "Change an App's language | Target: Android 13"
}
@end
--]=]

local config = {
  language = "en_",
  dpi = nil
}

local function setup_screen()
  local Configuration = import("android.content.res.Configuration")
  local Locale = import("java.util.Locale")

  local language_country = config.language:split("_")
  local lang = language_country[1]
  local country = language_country[2] or ""
  local locale = Locale(lang, country)

  hook {
    class = Configuration,
    returns = void,
    method = "setTo",
    params = { Configuration },
    after = function(this, params)
      this.densityDpi = config.dpi
      this:setLocale(locale)
    end
  }
end

setup_screen()

Is there anything I can do for you, ikws4?

LeeBinder commented 4 months ago

Hi ikws4/ Devin.

I now know why I hesitated till now to upgrade to Android 14.. because again Google must've added or changed some locale related hooks, and above script is only partially working anymore: only the UI of the app targeted via WeiJu2 with the change locale script reacts to the set locale (in my case "en_"), but the CONTENT remains in the language set in Android settings = the overall system language. Bummer, because above all it's the CONTENT which needs to be targeted via WeiJu here .. :(

Do you have any idea what these hooks might be and can share?

If not: maybe for this example we can stick with the app and specific version I'm trying to again have in EN because it was working before: Gaia (com.gaiamtv) v.4.3.5 (2834). Below links to download the apk:

Other than that, as before feel free to choose any app of your liking - but the script should work app-independent as long as the app contains the assigned locale (in this case en_), shouldn't it.

Hopefully you're running Android 14 yourself. And again simply let me know if you need any logs from me, and how to generate it/ them 👍