Skip to content

Commit

Permalink
Add a new lib-i18n to make message translation easier (#16942)
Browse files Browse the repository at this point in the history
* Add support to better internationalization with a new lib.

* Add info about `lib-i18n` in the contributing guide.

* Use lib-i18n in M+ as well.

* Change properties files to UTF-8.
  • Loading branch information
alessandrojean committed Jul 2, 2023
1 parent c4b08d0 commit 4e17c22
Show file tree
Hide file tree
Showing 20 changed files with 910 additions and 1,303 deletions.
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,16 @@ dependencies {
}
```
#### I18n library
[`lib-i18n`](https://github.com/tachiyomiorg/tachiyomi-extensions/tree/master/lib/i18n) is a library for handling internationalization in the sources. It allows loading `.properties` files with messages located under the `res/raw` folder of each extension, that can be used to translate strings under the source.
```gradle
dependencies {
implementation(project(':lib-i18n'))
}
```
#### Additional dependencies
If you find yourself needing additional functionality, you can add more dependencies to your `build.gradle` file.
Expand Down
21 changes: 21 additions & 0 deletions lib/i18n/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
plugins {
id("com.android.library")
kotlin("android")
}

android {
compileSdk = AndroidConfig.compileSdk

defaultConfig {
minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk
}
}

repositories {
mavenCentral()
}

dependencies {
compileOnly(libs.kotlin.stdlib)
}
2 changes: 2 additions & 0 deletions lib/i18n/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.lib.i18n" />
83 changes: 83 additions & 0 deletions lib/i18n/src/main/java/eu/kanade/tachiyomi/lib/i18n/Intl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package eu.kanade.tachiyomi.lib.i18n

import java.io.InputStreamReader
import java.text.Collator
import java.util.Locale
import java.util.PropertyResourceBundle

/**
* A simple wrapper to make internationalization easier to use in sources.
*
* Message files should be put at the `/res/raw` folder, with the name
* `messages_{iso_639_1}.properties`, where `iso_639_1` should be using
* snake case and be in lowercase.
*
* To edit the strings, use the official JetBrain's
* [Resource Bundle Editor plugin](https://plugins.jetbrains.com/plugin/17035-resource-bundle-editor).
*
* Make sure to configure Android Studio to save Properties files as UTF-8 as well.
* You can refer to this [documentation](https://www.jetbrains.com/help/idea/properties-files.html#1cbc434e)
* on how to do so.
*/
class Intl(
private val language: String,
private val baseLanguage: String,
private val availableLanguages: Set<String>,
private val classLoader: ClassLoader,
private val createMessageFileName: (String) -> String = { createDefaultMessageFileName(it) }
) {

val chosenLanguage: String = when (language) {
in availableLanguages -> language
else -> baseLanguage
}

private val locale: Locale = Locale.forLanguageTag(chosenLanguage)

val collator: Collator = Collator.getInstance(locale)

private val baseBundle: PropertyResourceBundle by lazy { createBundle(baseLanguage) }

private val bundle: PropertyResourceBundle by lazy {
if (chosenLanguage == baseLanguage) baseBundle else createBundle(chosenLanguage)
}

/**
* Returns the string from the message file. If the [key] is not present
* in the current language, the English value will be returned. If the [key]
* is also not present in English, the [key] surrounded by brackets will be returned.
*/
operator fun get(key: String): String = when {
bundle.containsKey(key) -> bundle.getString(key)
baseBundle.containsKey(key) -> baseBundle.getString(key)
else -> "[$key]"
}

fun languageDisplayName(localeCode: String): String =
Locale.forLanguageTag(localeCode)
.getDisplayName(locale)
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() }

/**
* Creates a [PropertyResourceBundle] instance from the language specified.
* The expected message file will be loaded from the `res/raw`.
*
* The [PropertyResourceBundle] is used directly instead of [java.util.ResourceBundle]
* because the later has issues with UTF-8 files in Java 8, which would need
* the message files to be saved in ISO-8859-1, making the file readability bad.
*/
private fun createBundle(lang: String): PropertyResourceBundle {
val fileName = createMessageFileName(lang)
val fileContent = classLoader.getResourceAsStream(fileName)

return PropertyResourceBundle(InputStreamReader(fileContent, "UTF-8"))
}

companion object {
fun createDefaultMessageFileName(lang: String): String {
val langSnakeCase = lang.replace("-", "_").lowercase()

return "res/raw/messages_$langSnakeCase.properties"
}
}
}
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
include(":core")

listOf("dataimage", "unpacker", "cryptoaes", "textinterceptor", "synchrony").forEach {
listOf("dataimage", "unpacker", "cryptoaes", "textinterceptor", "synchrony", "i18n").forEach {
include(":lib-$it")
project(":lib-$it").projectDir = File("lib/$it")
}
Expand Down
6 changes: 5 additions & 1 deletion src/all/mangadex/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ ext {
extName = 'MangaDex'
pkgNameSuffix = 'all.mangadex'
extClass = '.MangaDexFactory'
extVersionCode = 181
extVersionCode = 182
isNsfw = true
}

dependencies {
implementation(project(":lib-i18n"))
}

apply from: "$rootDir/common.gradle"
145 changes: 145 additions & 0 deletions src/all/mangadex/res/raw/messages_en.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
alternative_titles=Alternative titles:
alternative_titles_in_description=Alternative titles in description
alternative_titles_in_description_summary=Include a manga's alternative titles at the end of its description
block_group_by_uuid=Block groups by UUID
block_group_by_uuid_summary=Chapters from blocked groups will not show up in Latest or Manga feed. Enter as a Comma-separated list of group UUIDs
block_uploader_by_uuid=Block uploader by UUID
block_uploader_by_uuid_summary=Chapters from blocked uploaders will not show up in Latest or Manga feed. Enter as a Comma-separated list of uploader UUIDs
content=Content
content_gore=Gore
content_rating=Content rating
content_rating_erotica=Erotica
content_rating_genre=Content rating: %s
content_rating_pornographic=Pornographic
content_rating_safe=Safe
content_rating_suggestive=Suggestive
content_sexual_violence=Sexual violence
cover_quality=Cover quality
cover_quality_low=Low
cover_quality_medium=Medium
cover_quality_original=Original
data_saver=Data saver
data_saver_summary=Enables smaller, more compressed images
excluded_tags_mode=Excluded tags mode
filter_original_languages=Filter original languages
filter_original_languages_summary=Only show content that was originally published in the selected languages in both latest and browse
format=Format
format_adaptation=Adaptation
format_anthology=Anthology
format_award_winning=Award winning
format_doujinshi=Doujinshi
format_fan_colored=Fan colored
format_full_color=Full color
format_long_strip=Long strip
format_official_colored=Official colored
format_oneshot=Oneshot
format_user_created=User created
format_web_comic=Web comic
format_yonkoma=4-Koma
genre=Genre
genre_action=Action
genre_adventure=Adventure
genre_boys_love=Boy's Love
genre_comedy=Comedy
genre_crime=Crime
genre_drama=Drama
genre_fantasy=Fantasy
genre_girls_love=Girl's Love
genre_historical=Historical
genre_horror=Horror
genre_isekai=Isekai
genre_magical_girls=Magical girls
genre_mecha=Mecha
genre_medical=Medical
genre_mystery=Mystery
genre_philosophical=Philosophical
genre_romance=Romance
genre_sci_fi=Sci-Fi
genre_slice_of_life=Slice of life
genre_sports=Sports
genre_superhero=Superhero
genre_thriller=Thriller
genre_tragedy=Tragedy
genre_wuxia=Wuxia
has_available_chapters=Has available chapters
included_tags_mode=Included tags mode
invalid_author_id=Not a valid author ID
invalid_group_id=Not a valid group ID
invalid_uuids=The text contains invalid UUIDs
migrate_warning=Migrate this entry from MangaDex to MangaDex to update it
mode_and=And
mode_or=Or
no_group=No Group
no_series_in_list=No series in the list
original_language=Original language
original_language_filter_chinese=%s (Manhua)
original_language_filter_japanese=%s (Manga)
original_language_filter_korean=%s (Manhwa)
publication_demographic=Publication demographic
publication_demographic_josei=Josei
publication_demographic_none=None
publication_demographic_seinen=Seinen
publication_demographic_shoujo=Shoujo
publication_demographic_shounen=Shounen
sort=Sort
sort_alphabetic=Alphabetic
sort_chapter_uploaded_at=Chapter uploaded at
sort_content_created_at=Content created at
sort_content_info_updated_at=Content info updated at
sort_number_of_follows=Number of follows
sort_rating=Rating
sort_relevance=Relevance
sort_year=Year
standard_content_rating=Default content rating
standard_content_rating_summary=Show content with the selected ratings by default
standard_https_port=Use HTTPS port 443 only
standard_https_port_summary=Enable to only request image servers that use port 443. This allows users with stricter firewall restrictions to access MangaDex images
status=Status
status_cancelled=Cancelled
status_completed=Completed
status_hiatus=Hiatus
status_ongoing=Ongoing
tags_mode=Tags mode
theme=Theme
theme_aliens=Aliens
theme_animals=Animals
theme_cooking=Cooking
theme_crossdressing=Crossdressing
theme_delinquents=Delinquents
theme_demons=Demons
theme_gender_swap=Genderswap
theme_ghosts=Ghosts
theme_gyaru=Gyaru
theme_harem=Harem
theme_incest=Incest
theme_loli=Loli
theme_mafia=Mafia
theme_magic=Magic
theme_martial_arts=Martial arts
theme_military=Military
theme_monster_girls=Monster girls
theme_monsters=Monsters
theme_music=Music
theme_ninja=Ninja
theme_office_workers=Office workers
theme_police=Police
theme_post_apocalyptic=Post-apocalyptic
theme_psychological=Psychological
theme_reincarnation=Reincarnation
theme_reverse_harem=Reverse harem
theme_samurai=Samurai
theme_school_life=School life
theme_shota=Shota
theme_supernatural=Supernatural
theme_survival=Survival
theme_time_travel=Time travel
theme_traditional_games=Traditional games
theme_vampires=Vampires
theme_video_games=Video games
theme_villainess=Vilania
theme_virtual_reality=Virtual reality
theme_zombies=Zombies
try_using_first_volume_cover=Attempt to use the first volume cover as cover
try_using_first_volume_cover_summary=May need to manually refresh entries already in library. Otherwise, clear database to have new covers to show up.
unable_to_process_chapter_request=Unable to process Chapter request. HTTP code: %d
uploaded_by=Uploaded by %s
Loading

0 comments on commit 4e17c22

Please sign in to comment.