在海外发行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中添加。