Android 本地化适配 — 通过Excel、Room处理并缓存接口数据

在海外发行App,对App进行多语言适配是必不可少的。除了在项目中的strings.xml用到的文案外,还可能会在页面上显示接口返回的数据信息。如果出于某些原因后端没有时间处理多语言适配,App又急着发布版本,该怎么办呢?

先将涉及文案的接口数据全部缓存,然后前端在发起请求时在请求头中携带一个标记当前语言的参数,在没有做好适配时,后端判断请求中包含该参数时先返回500,前端使用本地缓存数据。适配好后,后端返回正常的数据。

准备翻译数据(Excel)

文案的翻译通常来说还是需要专业的翻译人员来处理才能确保翻译后文案的准确性。接口数据可能多且复杂,可以转换为Excel表格提供给翻译人员,也方便后续将翻译文案填入数据库。

测试了几个可以用于读写Excel的库,Apache POI、EasyEcxel不太适合在Android端使用(不支持AWT和Swing库,写入数据时会报错),最后选择了jxl,不过需要注意的是jxl仅支持.xls格式。

添加依赖库

在项目app module的build.gradle中的dependencies中添加依赖:

scss 复制代码
dependencies {
    implementation("net.sourceforge.jexcelapi:jxl:2.6.12")
}

创建Excel表格并写入数据

mock 100个博客信息作为接口返回信息,创建Excel并将博客信息写入,代码如下:

kotlin 复制代码
class RoomAndExcelExampleActivity : AppCompatActivity() {

    private lateinit var binding: LayoutExampleRoomAndExcelActivityBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = LayoutExampleRoomAndExcelActivityBinding.inflate(layoutInflater).also {
            setContentView(it.root)
        }

        binding.btnCreateExcel.setOnClickListener {
            writeGameDataToExcel()
        }
    }

    private fun writeGameDataToExcel() {
        val mockDataList = ArrayList<ArrayList<String>>().apply {
            repeat(100) {
                val index = it + 1
                add(arrayListOf("$index", "示例博客$index 封面", "示例博客$index 标题", "示例博客$index 简介", "示例博客$index 文案"))
            }
        }
        File(if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) else filesDir, "translation_example.xls").let {
            try {
                it.deleteOnExit()
                it.createNewFile()
                Workbook.createWorkbook(it).apply {
                    createSheet("translation", 0).let { writableSheet ->
                        for ((rowIndex, data) in mockDataList.withIndex()) {
                            for ((columnIndex, content) in data.withIndex()) {
                                if (columnIndex != 0) {
                                    // 设置列宽
                                    writableSheet.setColumnView(columnIndex, 30)
                                }
                                writableSheet.addCell(Label(columnIndex, rowIndex, content))
                            }
                        }
                    }
                    write()
                    close()
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }
}

运行后,可以在Device Explorer中找到生成的Excel,如下:

从Excel表格读取数据

生成Excel后,可以导出交给翻译人员翻译,将翻译后的Excel文件放到assets中读取出来,示例如下:

  • 翻译后Excel内容

封面一般是链接无需翻译,博客的标题、简介、正文有中英文两种版本。

  • 示例页面
kotlin 复制代码
class RoomAndExcelExampleActivity : AppCompatActivity() {

    ......

    private val textContentAdapter = TextDataAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        ......
        
        binding.btnReadExcel.setOnClickListener {
            binding.rvBlogContainer.adapter = textContentAdapter
            readDataFromExcel()
        }
    }

    ......

    private fun readDataFromExcel() {
        assets.open("translation_example.xls").use {
            Workbook.getWorkbook(it).getSheet(0).apply {
                val blogTranslationList = ArrayList<String>()
                for (rowIndex in 0 until rows) {
                    val titleTranslation = getCell(3, rowIndex).contents
                    val summaryTranslation = getCell(5, rowIndex).contents
                    val contentTranslation = getCell(7, rowIndex).contents
                    if (titleTranslation.isNotEmpty() && summaryTranslation.isNotEmpty() && contentTranslation.isNotEmpty()) {
                        blogTranslationList.add("title:$titleTranslation, summary:$summaryTranslation, content:$contentTranslation")
                    }
                }
                textContentAdapter.setNewData(blogTranslationList)
            }
        }
    }
}

效果如图:

使用翻译数据创建预装数据库(Room)

添加依赖库

在项目app module的build.gradle中的dependencies中添加依赖:

scss 复制代码
dependencies {
    implementation("androidx.room:room-runtime:2.6.1")
    implementation("androidx.room:room-ktx:2.6.1")
    
    // 使用kapt
    kapt("androidx.room:room-compiler:2.6.1")
    // 使用ksp
    ksp("androidx.room:room-compiler:2.6.1")
}

创建博客类和博客翻译类

  • 博客类
kotlin 复制代码
@Entity(tableName = TABLE_BLOG)
data class BlogExampleEntity(
    @PrimaryKey
    val id: String,
    val cover: String,
    val title: String,
    val summary: String,
    val content: String
) {

    override fun toString(): String {
        return "BlogExampleEntity(id='$id', cover='$cover', title='$title', summary='$summary', content='$content')"
    }
}
  • 博客翻译类
kotlin 复制代码
@Entity(tableName = TABLE_BLOG_TRANSLATION)
data class BlogTranslationExampleEntity(
    @PrimaryKey
    val id: String,
    val language: String,
    val title: String,
    val summary: String,
    val content :String
) {

    override fun toString(): String {
        return "BlogTranslationExampleEntity(id='$id', language='$language', title='$title', summary='$summary', content='$content')"
    }
}

创建读写接口

kotlin 复制代码
const val TABLE_BLOG = "blog"
const val TABLE_BLOG_TRANSLATION = "blog_translation"

@Dao
interface ExampleDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertBlogExample(game: List<BlogExampleEntity>)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertBlogExampleTranslation(gameTranslation: List<BlogTranslationExampleEntity>)

    /**
     * 结合两个表进行查询
     * 当前语言有翻译文案时使用翻译文案
     * 没有时使用默认文案
     */
    @Query("SELECT blog.id AS id, " +
            "blog.cover AS cover, " +
            "COALESCE(blogTranslation.title, blog.title) AS title, " +
            "COALESCE(blogTranslation.summary, blog.summary) AS summary, " +
            "COALESCE(blogTranslation.content, blog.content) AS content " +
            "FROM $TABLE_BLOG blog " +
            "LEFT JOIN $TABLE_BLOG_TRANSLATION blogTranslation ON blog.id = blogTranslation.id AND blogTranslation.language = :currentLanguage")
    suspend fun queryAllBlogWithLanguage(currentLanguage: String): List<BlogExampleEntity>

    /**
     * 结合两个表进行查询
     * 当前语言有翻译文案时使用翻译文案
     * 没有时使用默认文案
     */
    @Query("SELECT blog.id AS id, " +
            "blog.cover AS cover, " +
            "COALESCE(blogTranslation.title, blog.title) AS title, " +
            "COALESCE(blogTranslation.summary, blog.summary) AS summary, " +
            "COALESCE(blogTranslation.content, blog.content) AS content " +
            "FROM $TABLE_BLOG blog " +
            "LEFT JOIN $TABLE_BLOG_TRANSLATION blogTranslation ON blog.id = blogTranslation.id AND blogTranslation.language = :currentLanguage WHERE blog.id = :id")
    suspend fun queryBlogWitIdAndLanguage(id: String, currentLanguage: String): BlogExampleEntity?
}

创建数据库

kotlin 复制代码
@Database(entities = [
    BlogExampleEntity::class,
    BlogTranslationExampleEntity::class
], version = 1, exportSchema = false)
abstract class AbstractExampleLocalDatabase : RoomDatabase() {

    companion object {
        @Volatile
        var exampleLocalDataBase: AbstractExampleLocalDatabase? = null

        fun getInstance(context: Context): AbstractExampleLocalDatabase {
            if (exampleLocalDataBase == null) {
                synchronized(AbstractExampleLocalDatabase::class) {
                    if (exampleLocalDataBase == null) {
                        exampleLocalDataBase = buildDatabase(context)
                    }
                }
            }
            return exampleLocalDataBase!!
        }

        private fun buildDatabase(context: Context): AbstractExampleLocalDatabase {
            return Room.databaseBuilder(context, AbstractExampleLocalDatabase::class.java, "ExampleRoomDb")
                // 从assets中的预装数据库创建当前数据库
                // 预装数据库没准备好前可以先注释
                .createFromAsset("ExampleDatabase.db")
                .build()
        }
    }

    abstract fun getExampleDao(): ExampleDao
}

写入数据

将翻译数据写入数据库,代码如下:

scss 复制代码
class RoomAndExcelExampleActivity : AppCompatActivity() {

    ......

    private lateinit var exampleDao: ExampleDao

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        ......

        exampleDao = AbstractExampleLocalDatabase.getInstance(applicationContext).getExampleDao()

        binding.btnInsetBlogToDb.setOnClickListener {
            insertBlogToDb()
        }
    }
    
    ......

    private fun insertBlogToDb() {
        val blog = ArrayList<BlogExampleEntity>()
        val blogTranslation = ArrayList<BlogTranslationExampleEntity>()
        assets.open("translation_example.xls").use {
            Workbook.getWorkbook(it).getSheet(0).apply {
                for (rowIndex in 0 until rows) {
                    val id = getCell(0, rowIndex).contents
                    val cover = getCell(1, rowIndex).contents
                    val title = getCell(2, rowIndex).contents
                    val summary = getCell(4, rowIndex).contents
                    val content = getCell(6, rowIndex).contents
                    if (id.isNotEmpty() && cover.isNotEmpty() && title.isNotEmpty() && summary.isNotEmpty() && content.isNotEmpty()) {
                        blog.add(BlogExampleEntity(id, cover, title, summary, content))
                    }

                    val titleTranslation = getCell(3, rowIndex).contents
                    val summaryTranslation = getCell(5, rowIndex).contents
                    val contentTranslation = getCell(7, rowIndex).contents
                    if (id.isNotEmpty() && titleTranslation.isNotEmpty() && summaryTranslation.isNotEmpty() && contentTranslation.isNotEmpty()) {
                        blogTranslation.add(BlogTranslationExampleEntity(id, Locale.ENGLISH.language, titleTranslation, summaryTranslation, contentTranslation))
                    }
                }
            }
        }
        lifecycleScope.launch(Dispatchers.IO) {
            exampleDao.insertBlogExample(blog)
            exampleDao.insertBlogExampleTranslation(blogTranslation)
        }
    }
}

运行后,可以在App Inspectoin中查看数据库并导出,如下:

博客表 博客翻译表

读取数据

从数据库中读取数据,代码如下:

kotlin 复制代码
class RoomAndExcelExampleActivity : AppCompatActivity() {
    
    ......

    private lateinit var exampleDao: ExampleDao

    private val blogAdapter = BlogAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ......

        exampleDao = AbstractExampleLocalDatabase.getInstance(applicationContext).getExampleDao()

        binding.btnGetAllBlog.setOnClickListener {
            binding.rvBlogContainer.adapter = blogAdapter
            getAllBlog()
        }
        binding.btnGetBlogById.setOnClickListener {
            binding.rvBlogContainer.adapter = blogAdapter
            getBlogById()
        }
    }

    ......

    private fun getAllBlog() {
        lifecycleScope.launch(Dispatchers.IO) {
            exampleDao.queryAllBlogWithLanguage(Locale.getDefault().language).let {
                withContext(Dispatchers.Main) {
                    blogAdapter.setNewData(it)
                }
            }
        }
    }

    private fun getBlogById() {
        lifecycleScope.launch(Dispatchers.IO) {
            exampleDao.queryBlogWitIdAndLanguage("1", Locale.getDefault().language)?.let {
                withContext(Dispatchers.Main) {
                    blogAdapter.setNewData(arrayListOf(it))
                }
            }
        }
    }
}

效果如图:

示例

演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee

相关推荐
C4rpeDime1 小时前
自建MD5解密平台-续
android
鲤籽鲲3 小时前
C# Random 随机数 全面解析
android·java·c#
m0_548514777 小时前
2024.12.10——攻防世界Web_php_include
android·前端·php
凤邪摩羯7 小时前
Android-性能优化-03-启动优化-启动耗时
android
凤邪摩羯7 小时前
Android-性能优化-02-内存优化-LeakCanary原理解析
android
喀什酱豆腐8 小时前
Handle
android
m0_748232929 小时前
Android Https和WebView
android·网络协议·https
m0_748251729 小时前
Android webview 打开本地H5项目(Cocos游戏以及Unity游戏)
android·游戏·unity
m0_7482546611 小时前
go官方日志库带色彩格式化
android·开发语言·golang
zhangphil11 小时前
Android使用PorterDuffXfermode模式PorterDuff.Mode.SRC_OUT橡皮擦实现“刮刮乐”效果,Kotlin(2)
android·kotlin