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

相关推荐
帅得不敢出门6 分钟前
Gradle命令编译Android Studio工程项目并签名
android·ide·android studio·gradlew
problc1 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter
帅得不敢出门11 小时前
安卓设备adb执行AT指令控制电话卡
android·adb·sim卡·at指令·电话卡
我又来搬代码了13 小时前
【Android】使用productFlavors构建多个变体
android
德育处主任14 小时前
Mac和安卓手机互传文件(ADB)
android·macos
芦半山14 小时前
Android“引用们”的底层原理
android·java
迃-幵15 小时前
力扣:225 用队列实现栈
android·javascript·leetcode
大风起兮云飞扬丶15 小时前
Android——从相机/相册获取图片
android
Rverdoser16 小时前
Android Studio 多工程公用module引用
android·ide·android studio
aaajj16 小时前
[Android]从FLAG_SECURE禁止截屏看surface
android