├── sample ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── values │ │ │ ├── dimens.xml │ │ │ ├── attrs.xml │ │ │ ├── colors.xml │ │ │ ├── styles.xml │ │ │ └── strings.xml │ │ ├── values-land │ │ │ └── dimens.xml │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── layout │ │ │ ├── layout_custom_view.xml │ │ │ └── activity_main.xml │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ └── drawable │ │ │ └── ic_launcher_background.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── jcminarro │ │ └── philology │ │ └── sample │ │ ├── view │ │ └── CustomView.kt │ │ ├── MainActivity.kt │ │ └── App.kt ├── proguard-rules.pro └── build.gradle ├── philology ├── .gitignore ├── src │ ├── test │ │ ├── resources │ │ │ └── mockito-extensions │ │ │ │ └── org.mockito.plugins.MockMaker │ │ └── java │ │ │ ├── android │ │ │ └── content │ │ │ │ └── ContextWrapper.kt │ │ │ └── com │ │ │ └── jcminarro │ │ │ └── philology │ │ │ ├── transformer │ │ │ ├── NoneViewTransformerTest.kt │ │ │ ├── TextViewTransformerTest.kt │ │ │ ├── ToolbarViewTransformerTest.kt │ │ │ └── SupportToolbarViewTransformerTest.kt │ │ │ ├── PhilologyContextWrapperTest.kt │ │ │ ├── PhilologyTest.kt │ │ │ ├── PhilologyVectorEnabledTintResourcesTest.kt │ │ │ ├── Mother.kt │ │ │ ├── ResourcesUtilTest.kt │ │ │ └── PhilologyResourcesTest.kt │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── jcminarro │ │ │ └── philology │ │ │ ├── transformer │ │ │ ├── NoneViewTransformer.kt │ │ │ ├── TextViewTransformer.kt │ │ │ ├── SupportToolbarViewTransformer.kt │ │ │ └── ToolbarViewTransformer.kt │ │ │ ├── ViewTransformer.kt │ │ │ ├── PhilologyVectorEnabledTintResources.kt │ │ │ ├── PhilologyContextWrapper.kt │ │ │ ├── PhilologyInterceptor.kt │ │ │ ├── ResourcesUtil.kt │ │ │ ├── Philology.kt │ │ │ └── PhilologyResources.kt │ │ └── res │ │ └── values │ │ └── strings.xml ├── proguard-rules.pro ├── deploy.gradle └── build.gradle ├── settings.gradle ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── robolectric.gradle ├── gradlew.bat ├── CHANGELOG.md ├── .circleci └── config.yml ├── gradlew ├── README.md └── LICENSE /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /philology/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':sample', ':philology' 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jcminarro 2 | ko_fi: jcminarro 3 | -------------------------------------------------------------------------------- /philology/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JcMinarro/Philology/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JcMinarro/Philology/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JcMinarro/Philology/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JcMinarro/Philology/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JcMinarro/Philology/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JcMinarro/Philology/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JcMinarro/Philology/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JcMinarro/Philology/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JcMinarro/Philology/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50dp 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JcMinarro/Philology/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JcMinarro/Philology/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/values-land/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 150dp 4 | 5 | -------------------------------------------------------------------------------- /philology/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Android Studio 2 | .idea 3 | *.iml 4 | *.ipr 5 | *.iws 6 | classes 7 | gen-external-apklibs 8 | 9 | #Gradle 10 | .gradle 11 | local.properties 12 | build 13 | 14 | #Other 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /sample/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /philology/src/test/java/android/content/ContextWrapper.kt: -------------------------------------------------------------------------------- 1 | package android.content 2 | 3 | import android.content.res.Resources 4 | 5 | abstract class ContextWrapper(private val base: Context) : Context() { 6 | override fun getResources(): Resources = base.resources 7 | } -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /philology/src/main/java/com/jcminarro/philology/transformer/NoneViewTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology.transformer 2 | 3 | import android.util.AttributeSet 4 | import android.view.View 5 | import com.jcminarro.philology.ViewTransformer 6 | 7 | object NoneViewTransformer : ViewTransformer { 8 | override fun reword(view: View, attributeSet: AttributeSet): View = view 9 | } 10 | -------------------------------------------------------------------------------- /philology/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | zero 4 | one 5 | two 6 | few 7 | many 8 | other 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/layout_custom_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: 'jcminarro' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: 'jcminarro' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | How can we reproduce this? 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Logs/Screenshots** 20 | If applicable, add logs or screenshots to help explain your problem. 21 | 22 | **Library version** 23 | x.y.z 24 | -------------------------------------------------------------------------------- /philology/src/main/java/com/jcminarro/philology/ViewTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import androidx.annotation.AttrRes 7 | import androidx.annotation.StringRes 8 | 9 | interface ViewTransformer { 10 | fun reword(view: View, attributeSet: AttributeSet): View 11 | 12 | @StringRes 13 | fun Context.getStringResourceId( 14 | attributeSet: AttributeSet, 15 | @AttrRes attrResId: Int 16 | ): Int { 17 | val typedArray = obtainStyledAttributes(attributeSet, intArrayOf(attrResId)) 18 | val stringResId = typedArray.getResourceId(0, -1) 19 | typedArray.recycle() 20 | return stringResId 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /philology/src/main/java/com/jcminarro/philology/PhilologyVectorEnabledTintResources.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.res.Resources 6 | import androidx.appcompat.widget.VectorEnabledTintResources 7 | 8 | @SuppressWarnings("RestrictedApi") 9 | @SuppressLint("RestrictedApi") 10 | internal class PhilologyVectorEnabledTintResources(baseContext: Context, baseResources: Resources) 11 | : VectorEnabledTintResources(baseContext, baseResources) { 12 | private val resourcesUtil = ResourcesUtil(baseResources) 13 | 14 | override fun getText(id: Int): CharSequence = resourcesUtil.getText(id) 15 | override fun getString(id: Int): String = resourcesUtil.getString(id) 16 | } -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /philology/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /philology/src/test/java/com/jcminarro/philology/transformer/NoneViewTransformerTest.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology.transformer 2 | 3 | import android.util.AttributeSet 4 | import android.view.View 5 | import org.amshove.kluent.`Verify no further interactions` 6 | import org.amshove.kluent.`should be` 7 | import org.amshove.kluent.mock 8 | import org.amshove.kluent.on 9 | import org.junit.Test 10 | 11 | class NoneViewTransformerTest { 12 | private val view: View = mock() 13 | private val attributeSet: AttributeSet = mock() 14 | 15 | @Test 16 | fun `View should be the same`() { 17 | NoneViewTransformer.reword(view, attributeSet) `should be` view 18 | } 19 | 20 | @Test 21 | fun `View shouldn't be modified`() { 22 | NoneViewTransformer.reword(view, attributeSet) 23 | 24 | `Verify no further interactions` on view 25 | } 26 | } -------------------------------------------------------------------------------- /philology/src/main/java/com/jcminarro/philology/PhilologyContextWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import android.content.res.Resources 7 | import androidx.appcompat.widget.VectorEnabledTintResources 8 | 9 | @SuppressWarnings("RestrictedApi") 10 | @SuppressLint("RestrictedApi") 11 | internal class PhilologyContextWrapper(base: Context) : ContextWrapper(base) { 12 | private val res: Resources by lazy { 13 | val baseResources = super.getResources() 14 | if (VectorEnabledTintResources.shouldBeUsed()) { 15 | PhilologyVectorEnabledTintResources(this, baseResources) 16 | } else { 17 | PhilologyResources(baseResources) 18 | } 19 | } 20 | 21 | override fun getResources(): Resources = res 22 | } -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 14 | 15 | 18 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | 15 | # Bintray credential 16 | BINTRAY_API_KEY=SOME_API_KEY 17 | BINTRAY_USER=SOME_USER 18 | android.useAndroidX=true 19 | android.enableJetifier=true -------------------------------------------------------------------------------- /philology/src/main/java/com/jcminarro/philology/PhilologyInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology 2 | 3 | import android.util.AttributeSet 4 | import android.view.View 5 | import io.github.inflationx.viewpump.InflateResult 6 | import io.github.inflationx.viewpump.Interceptor 7 | 8 | object PhilologyInterceptor : Interceptor { 9 | override fun intercept(chain: Interceptor.Chain): InflateResult = 10 | chain.proceed(chain.request()).let {inflateResult -> 11 | inflateResult.toBuilder() 12 | .view(rewordView(inflateResult.view, inflateResult.attrs)) 13 | .build() 14 | } 15 | 16 | private fun rewordView(view: View?, attributeSet: AttributeSet?): View? = when { 17 | view != null && attributeSet != null -> view.reword(attributeSet) 18 | else -> view 19 | } 20 | } 21 | 22 | private fun View.reword(attrs: AttributeSet): View = Philology.getViewTransformer(this).reword(this, attrs) 23 | -------------------------------------------------------------------------------- /philology/src/main/java/com/jcminarro/philology/transformer/TextViewTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology.transformer 2 | 3 | import android.util.AttributeSet 4 | import android.view.View 5 | import android.widget.TextView 6 | import androidx.annotation.StringRes 7 | import com.jcminarro.philology.ViewTransformer 8 | 9 | internal object TextViewTransformer : ViewTransformer { 10 | override fun reword(view: View, attributeSet: AttributeSet): View = view.apply { 11 | when (this) { 12 | is TextView -> reword(attributeSet) 13 | } 14 | } 15 | 16 | private fun TextView.reword(attributeSet: AttributeSet) { 17 | @StringRes val textResId = 18 | context.getStringResourceId(attributeSet, android.R.attr.text) 19 | @StringRes val hintResId = 20 | context.getStringResourceId(attributeSet, android.R.attr.hint) 21 | 22 | if (textResId > 0) setText(textResId) 23 | if (hintResId > 0) setHint(hintResId) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Philology Sample 3 | Philology Sample 4 | It is the default value that we have into our strings resource file 5 | Plural quantity edit 6 | Plural quantity format edit 7 | 8 | One test 9 | Other tests 10 | 11 | 12 | %d test 13 | %d tests 14 | 15 | 16 | Monday 17 | Tuesday 18 | Wednesday 19 | Thursday 20 | Friday 21 | Saturday 22 | Saturday 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/java/com/jcminarro/philology/sample/view/CustomView.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology.view 2 | 3 | import android.content.Context 4 | import android.content.res.Configuration 5 | import android.util.AttributeSet 6 | import android.view.View 7 | import android.widget.FrameLayout 8 | import android.widget.TextView 9 | import com.jcminarro.sample.R 10 | 11 | class CustomView @JvmOverloads constructor( 12 | context: Context, 13 | attrs: AttributeSet? = null, 14 | defStyleAttr: Int = 0 15 | ) : FrameLayout(context, attrs, defStyleAttr) { 16 | private val textView: TextView by lazy { 17 | findViewById(R.id.text) 18 | } 19 | 20 | init { 21 | View.inflate(context, R.layout.layout_custom_view, this) 22 | val width = resources.getDimensionPixelSize(R.dimen.text_width) 23 | textView.text = "Width = $width" 24 | } 25 | 26 | override fun onConfigurationChanged(newConfig: Configuration?) { 27 | super.onConfigurationChanged(newConfig) 28 | val width = resources.getDimensionPixelSize(R.dimen.text_width) 29 | textView.text = "Width = $width" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /philology/src/test/java/com/jcminarro/philology/PhilologyContextWrapperTest.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import androidx.appcompat.app.AppCompatDelegate 6 | import com.nhaarman.mockito_kotlin.doReturn 7 | import org.amshove.kluent.When 8 | import org.amshove.kluent.`should be instance of` 9 | import org.amshove.kluent.calling 10 | import org.amshove.kluent.mock 11 | import org.junit.Before 12 | import org.junit.Test 13 | 14 | class PhilologyContextWrapperTest { 15 | 16 | val baseContext: Context = mock() 17 | val baseResources: Resources = mock() 18 | private val philologyContextWrapper = PhilologyContextWrapper(baseContext) 19 | 20 | @Before 21 | fun setup() { 22 | When calling baseContext.resources doReturn baseResources 23 | } 24 | 25 | @Test 26 | fun `Should return a PhilologyResources`() { 27 | AppCompatDelegate.setCompatVectorFromResourcesEnabled(false) 28 | philologyContextWrapper.resources `should be instance of` PhilologyResources::class 29 | } 30 | 31 | @Test 32 | fun `Should return a PhilologyVectorEnabledTintResources`() { 33 | AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) 34 | philologyContextWrapper.resources `should be instance of` PhilologyVectorEnabledTintResources::class 35 | } 36 | } -------------------------------------------------------------------------------- /philology/src/main/java/com/jcminarro/philology/transformer/SupportToolbarViewTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology.transformer 2 | 3 | import android.util.AttributeSet 4 | import android.view.View 5 | import androidx.annotation.StringRes 6 | import androidx.appcompat.widget.Toolbar 7 | import com.jcminarro.philology.ViewTransformer 8 | 9 | internal object SupportToolbarViewTransformer : ViewTransformer { 10 | override fun reword(view: View, attributeSet: AttributeSet): View = view.apply { 11 | when (this) { 12 | is Toolbar -> reword(attributeSet) 13 | } 14 | } 15 | 16 | private fun Toolbar.reword(attributeSet: AttributeSet) { 17 | @StringRes val titleResId = 18 | context.getStringResourceId(attributeSet, android.R.attr.title) 19 | @StringRes val titleCompatResId = 20 | context.getStringResourceId(attributeSet, androidx.appcompat.R.attr.title) 21 | @StringRes val subTitleResId = 22 | context.getStringResourceId(attributeSet, android.R.attr.subtitle) 23 | @StringRes val subTitleCompatResId = 24 | context.getStringResourceId(attributeSet, androidx.appcompat.R.attr.subtitle) 25 | 26 | if (titleResId > 0) setTitle(titleResId) 27 | else if (titleCompatResId > 0) setTitle(titleCompatResId) 28 | 29 | if (subTitleResId > 0) setSubtitle(subTitleResId) 30 | else if (subTitleCompatResId > 0) setSubtitle(subTitleCompatResId) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /robolectric.gradle: -------------------------------------------------------------------------------- 1 | import com.jcminarro.AndroidSdk 2 | 3 | def allSdkConfigurations = [] 4 | 5 | task prefetchSdks() { 6 | AndroidSdk.ALL_SDKS.each { androidSdk -> 7 | def config = configurations.create("sdk${androidSdk.apiLevel}") 8 | dependencies.add("sdk${androidSdk.apiLevel}", androidSdk.coordinates) 9 | // causes dependencies to be resolved: 10 | config.files 11 | allSdkConfigurations << config.files.first() 12 | } 13 | } 14 | 15 | task prefetchDependencies() { 16 | dependsOn "prefetchSdks" 17 | 18 | doLast { 19 | allprojects.each { p -> 20 | ['compile', 'runtime', 'testCompile', 'testRuntime'].each { configName -> 21 | if (p.configurations.findByName(configName)) { 22 | // causes dependencies to be resolved: 23 | p.configurations[configName].files 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | task copyRobolectricSdk() { 31 | dependsOn prefetchDependencies 32 | doLast { 33 | def robolectricSdkFolder = new File("$buildDir/robolectricSdk") 34 | robolectricSdkFolder.mkdirs() 35 | allSdkConfigurations.forEach { filePath -> 36 | copy { 37 | from filePath 38 | into "$buildDir/robolectricSdk" 39 | } 40 | } 41 | } 42 | } 43 | 44 | subprojects.forEach{ 45 | it.getTasksByName("test", true).forEach{ 46 | it.dependsOn copyRobolectricSdk 47 | } 48 | } -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | import com.jcminarro.Dependencies 2 | import com.jcminarro.Config 3 | 4 | apply plugin: 'com.android.application' 5 | apply plugin: 'kotlin-android' 6 | apply plugin: 'kotlin-android-extensions' 7 | 8 | android { 9 | compileSdkVersion Config.compileSdkVersion 10 | defaultConfig { 11 | applicationId "com.jcminarro.philology.sample" 12 | minSdkVersion Config.minSdkVersion 13 | targetSdkVersion Config.targetSdkVersion 14 | versionCode Config.versionCode 15 | versionName Config.versionName 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation project(":philology") 28 | 29 | implementation Dependencies.kotlinSTDLib 30 | implementation Dependencies.appCompat 31 | implementation Dependencies.viewPump 32 | 33 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 34 | testImplementation Dependencies.jUnit 35 | testImplementation Dependencies.mockito 36 | testImplementation Dependencies.mockitoKotlin 37 | testImplementation Dependencies.kotlinTestJunit 38 | testImplementation Dependencies.kluent 39 | 40 | androidTestImplementation Dependencies.AndroidTestRunner 41 | androidTestImplementation Dependencies.espresso 42 | } 43 | -------------------------------------------------------------------------------- /philology/src/main/java/com/jcminarro/philology/transformer/ToolbarViewTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology.transformer 2 | 3 | import android.annotation.SuppressLint 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.widget.Toolbar 7 | import androidx.annotation.StringRes 8 | import com.jcminarro.philology.ViewTransformer 9 | 10 | @SuppressLint("NewApi") 11 | internal object ToolbarViewTransformer : ViewTransformer { 12 | override fun reword(view: View, attributeSet: AttributeSet): View = view.apply { 13 | when (this) { 14 | is Toolbar -> reword(attributeSet) 15 | } 16 | } 17 | 18 | private fun Toolbar.reword(attributeSet: AttributeSet) { 19 | @StringRes val titleResId = 20 | context.getStringResourceId(attributeSet, android.R.attr.title) 21 | @StringRes val titleCompatResId = 22 | context.getStringResourceId(attributeSet, androidx.appcompat.R.attr.title) 23 | @StringRes val subTitleResId = 24 | context.getStringResourceId(attributeSet, android.R.attr.subtitle) 25 | @StringRes val subTitleCompatResId = 26 | context.getStringResourceId(attributeSet, androidx.appcompat.R.attr.subtitle) 27 | 28 | if (titleResId > 0) setTitle(titleResId) 29 | else if (titleCompatResId > 0) setTitle(titleCompatResId) 30 | 31 | if (subTitleResId > 0) setSubtitle(subTitleResId) 32 | else if (subTitleCompatResId > 0) setSubtitle(subTitleCompatResId) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /philology/deploy.gradle: -------------------------------------------------------------------------------- 1 | import com.jcminarro.Config 2 | 3 | apply plugin: 'maven' 4 | 5 | uploadArchives { 6 | repositories { 7 | mavenDeployer { 8 | repository(id: 'bintray-jcminarro-maven', url: "https://api.bintray.com/maven/jcminarro/maven/$Config.pomArtifactId") { 9 | authentication(userName: BINTRAY_USER, password: BINTRAY_API_KEY) 10 | } 11 | pom.project { 12 | groupId=Config.group 13 | version=Config.versionName 14 | artifactId Config.pomArtifactId 15 | name Config.pomName 16 | description Config.pomDescription 17 | url Config.pomURL 18 | 19 | licenses { 20 | license { 21 | name Config.pomLicenceName 22 | url Config.pomLicenceUrl 23 | distribution Config.pomLicenceDist 24 | } 25 | } 26 | 27 | scm { 28 | url Config.pomSCMURL 29 | connection Config.pomSCMConnection 30 | developerConnection Config.pomSCMCDevonnection 31 | } 32 | 33 | developers { 34 | developer { 35 | id Config.pomDeveloperId 36 | name Config.pomDeveloperName 37 | email Config.pomDeveloperEmail 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | task sourcesJar(type: Jar) { 46 | from android.sourceSets.main.java.srcDirs 47 | classifier = 'sources' 48 | } -------------------------------------------------------------------------------- /sample/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 8 | 14 | 15 | 22 | 26 | 30 | 31 | 32 | 33 | 40 | 41 | -------------------------------------------------------------------------------- /sample/src/main/java/com/jcminarro/philology/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology.sample 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.text.Editable 6 | import android.text.TextWatcher 7 | import android.widget.EditText 8 | import android.widget.TextView 9 | import androidx.appcompat.app.AppCompatActivity 10 | import com.jcminarro.philology.Philology 11 | import com.jcminarro.sample.R 12 | import io.github.inflationx.viewpump.ViewPumpContextWrapper 13 | 14 | class MainActivity : AppCompatActivity() { 15 | 16 | override fun attachBaseContext(newBase: Context) { 17 | super.attachBaseContext(ViewPumpContextWrapper.wrap(Philology.wrap(newBase))) 18 | } 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(R.layout.activity_main) 23 | val pluralLabel = findViewById(R.id.plural_label) 24 | findViewById(R.id.plural_quantity_edit).afterTextChanged { text: String -> 25 | text.toIntOrNull()?.let { 26 | pluralLabel.text = resources.getQuantityString(R.plurals.plurals_sample, it) 27 | } 28 | } 29 | 30 | val pluralFormatLabel = findViewById(R.id.plural_format_label) 31 | findViewById(R.id.plural_quantity_format_edit).afterTextChanged { text: String -> 32 | text.toIntOrNull()?.let { 33 | pluralFormatLabel.text = resources.getQuantityString(R.plurals.plurals_sample_format, it, it) 34 | } 35 | } 36 | } 37 | 38 | private fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) { 39 | this.addTextChangedListener(object : TextWatcher { 40 | override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { 41 | } 42 | 43 | override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { 44 | } 45 | 46 | override fun afterTextChanged(editable: Editable?) { 47 | afterTextChanged.invoke(editable.toString()) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /philology/build.gradle: -------------------------------------------------------------------------------- 1 | import com.jcminarro.Config 2 | import com.jcminarro.Dependencies 3 | 4 | apply plugin: 'com.android.library' 5 | apply plugin: 'kotlin-android' 6 | apply plugin: 'kotlin-android-extensions' 7 | apply from: 'deploy.gradle' 8 | 9 | android { 10 | compileSdkVersion Config.compileSdkVersion 11 | 12 | defaultConfig { 13 | minSdkVersion Config.minSdkVersion 14 | targetSdkVersion Config.targetSdkVersion 15 | versionCode Config.versionCode 16 | versionName Config.versionName 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | 27 | testOptions{ 28 | unitTests{ 29 | includeAndroidResources = true 30 | all { 31 | systemProperty 'robolectric.offline', 'true' 32 | systemProperty 'robolectric.dependency.dir', "$rootDir/build/robolectricSdk" 33 | testLogging { 34 | events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' 35 | } 36 | setMaxParallelForks(Runtime.runtime.availableProcessors() ?: 1) 37 | } 38 | } 39 | } 40 | 41 | compileOptions { 42 | sourceCompatibility JavaVersion.VERSION_1_8 43 | targetCompatibility JavaVersion.VERSION_1_8 44 | } 45 | 46 | kotlinOptions { 47 | jvmTarget = "1.8" 48 | } 49 | } 50 | 51 | dependencies { 52 | implementation Dependencies.kotlinSTDLib 53 | implementation Dependencies.appCompat 54 | implementation Dependencies.viewPump 55 | 56 | testImplementation Dependencies.robolectric 57 | testImplementation Dependencies.jUnit 58 | testImplementation Dependencies.mockito 59 | testImplementation Dependencies.mockitoKotlin 60 | testImplementation Dependencies.kotlinTestJunit 61 | testImplementation Dependencies.kluent 62 | 63 | androidTestImplementation Dependencies.AndroidTestRunner 64 | androidTestImplementation Dependencies.espresso 65 | } 66 | -------------------------------------------------------------------------------- /philology/src/main/java/com/jcminarro/philology/ResourcesUtil.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology 2 | 3 | import android.content.res.Resources 4 | import android.icu.text.PluralRules 5 | import android.os.Build 6 | import java.util.Locale 7 | 8 | internal class ResourcesUtil(private val baseResources: Resources) { 9 | private val repository: PhilologyRepository by lazy { 10 | Philology.getPhilologyRepository(baseResources.currentLocale()) 11 | } 12 | 13 | @Throws(Resources.NotFoundException::class) 14 | fun getText(id: Int): CharSequence { 15 | return repository.getText(baseResources.getResourceEntryName(id)) 16 | ?: baseResources.getText(id) 17 | } 18 | 19 | @Throws(Resources.NotFoundException::class) 20 | fun getString(id: Int): String = getText(id).toString() 21 | 22 | @Throws(Resources.NotFoundException::class) 23 | fun getQuantityText(id: Int, quantity: Int): CharSequence = repository.getPlural( 24 | baseResources.getResourceEntryName(id), 25 | quantity.toPluralKeyword(baseResources) 26 | ) ?: baseResources.getQuantityText(id, quantity) 27 | 28 | @Throws(Resources.NotFoundException::class) 29 | fun getQuantityString(id: Int, quantity: Int): String = getQuantityText(id, quantity).toString() 30 | 31 | @Throws(Resources.NotFoundException::class) 32 | fun getQuantityString(id: Int, quantity: Int, vararg formatArgs: Any?): String = 33 | String.format(getQuantityString(id, quantity), *formatArgs) 34 | 35 | fun getStringArray(id: Int): Array = 36 | getTextArray(id).map { it.toString() }.toTypedArray() 37 | 38 | fun getTextArray(id: Int): Array { 39 | return repository.getTextArray(baseResources.getResourceEntryName(id)) 40 | ?: baseResources.getTextArray(id) 41 | } 42 | } 43 | 44 | interface PhilologyRepository { 45 | fun getText(key: String): CharSequence? = null 46 | fun getPlural(key: String, quantityString: String): CharSequence? = null 47 | fun getTextArray(key: String): Array? = null 48 | } 49 | 50 | @SuppressWarnings("NewApi") 51 | private fun Resources.currentLocale(): Locale = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { 52 | @Suppress("DEPRECATION") 53 | configuration.locale 54 | } else { 55 | configuration.locales[0] 56 | } 57 | 58 | private fun Int.toPluralKeyword(baseResources: Resources): String = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 59 | PluralRules.forLocale(baseResources.currentLocale()).select(this.toDouble()) 60 | } else { 61 | baseResources.getQuantityString(R.plurals.com_jcminarro_philology_quantity_string, this) 62 | } 63 | -------------------------------------------------------------------------------- /philology/src/test/java/com/jcminarro/philology/PhilologyTest.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology 2 | 3 | import android.os.Build.VERSION_CODES.JELLY_BEAN 4 | import android.os.Build.VERSION_CODES.JELLY_BEAN_MR1 5 | import android.os.Build.VERSION_CODES.JELLY_BEAN_MR2 6 | import android.os.Build.VERSION_CODES.KITKAT 7 | import android.os.Build.VERSION_CODES.LOLLIPOP 8 | import android.os.Build.VERSION_CODES.LOLLIPOP_MR1 9 | import android.os.Build.VERSION_CODES.M 10 | import android.os.Build.VERSION_CODES.N 11 | import android.os.Build.VERSION_CODES.N_MR1 12 | import android.os.Build.VERSION_CODES.O 13 | import android.os.Build.VERSION_CODES.O_MR1 14 | import androidx.appcompat.widget.Toolbar 15 | import android.widget.TextView 16 | import com.jcminarro.philology.transformer.NoneViewTransformer 17 | import com.jcminarro.philology.transformer.SupportToolbarViewTransformer 18 | import com.jcminarro.philology.transformer.TextViewTransformer 19 | import com.jcminarro.philology.transformer.ToolbarViewTransformer 20 | import org.amshove.kluent.`should be instance of` 21 | import org.amshove.kluent.`should be` 22 | import org.amshove.kluent.mock 23 | import org.junit.Test 24 | import org.junit.runner.RunWith 25 | import org.robolectric.RobolectricTestRunner 26 | import org.robolectric.RuntimeEnvironment.application 27 | import org.robolectric.annotation.Config 28 | 29 | @RunWith(RobolectricTestRunner::class) 30 | @Config(sdk = [JELLY_BEAN, JELLY_BEAN_MR1, JELLY_BEAN_MR2, KITKAT, LOLLIPOP, LOLLIPOP_MR1, M, N, N_MR1, O, O_MR1]) 31 | class PhilologyTest { 32 | 33 | @Test 34 | fun `Should return a PhilologyContextWrapper`() { 35 | Philology.wrap(application) `should be instance of` PhilologyContextWrapper::class 36 | } 37 | 38 | @Test 39 | fun `Should provide a NonViewTransformer`() { 40 | Philology.getViewTransformer(mock()) `should be` NoneViewTransformer 41 | } 42 | 43 | @Test 44 | fun `Should provide a SupportToolbarViewTransformer`() { 45 | Philology.getViewTransformer(mock()) `should be` SupportToolbarViewTransformer 46 | } 47 | 48 | @Test 49 | @Config(sdk = [LOLLIPOP, LOLLIPOP_MR1, M, N, N_MR1, O, O_MR1]) 50 | fun `Should provide a ToolbarViewTransformer`() { 51 | Philology.getViewTransformer(mock()) `should be` ToolbarViewTransformer 52 | } 53 | 54 | @Test 55 | @Config(sdk = [JELLY_BEAN, JELLY_BEAN_MR1, JELLY_BEAN_MR2, KITKAT]) 56 | fun `Shouldn't provide a ToolbarViewTransformer because Toolbar doesn't exist on those APIs`() { 57 | Philology.getViewTransformer(mock()) `should be` NoneViewTransformer 58 | } 59 | 60 | @Test 61 | fun `Should provide a TextViewTransformer`() { 62 | Philology.getViewTransformer(mock()) `should be` TextViewTransformer 63 | } 64 | } -------------------------------------------------------------------------------- /philology/src/test/java/com/jcminarro/philology/PhilologyVectorEnabledTintResourcesTest.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology 2 | 3 | import android.content.Context 4 | import android.content.res.Configuration 5 | import android.content.res.Resources 6 | import com.nhaarman.mockito_kotlin.doReturn 7 | import org.amshove.kluent.When 8 | import org.amshove.kluent.`should be equal to` 9 | import org.amshove.kluent.`should throw` 10 | import org.amshove.kluent.calling 11 | import org.amshove.kluent.invoking 12 | import org.amshove.kluent.mock 13 | import org.junit.Before 14 | import org.junit.Test 15 | 16 | class PhilologyVectorEnabledTintResourcesTest { 17 | 18 | private val baseResources: Resources = mock() 19 | private val baseContext: Context = mock() 20 | private val configuration: Configuration = createConfiguration() 21 | private val resources = PhilologyVectorEnabledTintResources(baseContext, baseResources) 22 | private val someCharSequence: CharSequence = "text" 23 | private val someString: String = someCharSequence.toString() 24 | private val repoCharSequence: CharSequence = "repo" 25 | private val repoString: String = repoCharSequence.toString() 26 | private val id = 0 27 | private val nameId = "nameId" 28 | 29 | @Before 30 | fun setup() { 31 | clearPhilology() 32 | When calling baseResources.configuration doReturn configuration 33 | } 34 | 35 | @Test 36 | fun `Should throw an exception if the given id doesn't exit asking for a text`() { 37 | configureResourceGetIdException(baseResources, id) 38 | invoking { resources.getText(id) } `should throw` Resources.NotFoundException::class 39 | } 40 | 41 | @Test 42 | fun `Should return a CharSecuence asking for a text`() { 43 | configureResourceGetText(baseResources, id, nameId, someCharSequence) 44 | resources.getText(id) `should be equal to` someCharSequence 45 | } 46 | 47 | @Test 48 | fun `Should throw an exception if the given id doesn't exit asking for an String`() { 49 | configureResourceGetIdException(baseResources, id) 50 | invoking { resources.getString(id) } `should throw` Resources.NotFoundException::class 51 | } 52 | 53 | @Test 54 | fun `Should return a CharSecuence asking for an String`() { 55 | configureResourceGetText(baseResources, id, nameId, someCharSequence) 56 | resources.getString(id) `should be equal to` someString 57 | } 58 | 59 | @Test 60 | fun `Should return a CharSecuence from repository asking for a text`() { 61 | configureResourceGetText(baseResources, id, nameId, someCharSequence) 62 | configurePhilology(createRepository(nameId, null, repoCharSequence)) 63 | resources.getText(id) `should be equal to` repoCharSequence 64 | } 65 | 66 | @Test 67 | fun `Should return a CharSecuence from repository asking for an String`() { 68 | configureResourceGetText(baseResources, id, nameId, someCharSequence) 69 | configurePhilology(createRepository(nameId, null, repoCharSequence)) 70 | resources.getString(id) `should be equal to` repoString 71 | } 72 | } -------------------------------------------------------------------------------- /philology/src/main/java/com/jcminarro/philology/Philology.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology 2 | 3 | import android.content.Context 4 | import android.content.ContextWrapper 5 | import android.os.Build 6 | import androidx.appcompat.widget.Toolbar 7 | import android.view.View 8 | import android.widget.TextView 9 | import com.jcminarro.philology.transformer.NoneViewTransformer 10 | import com.jcminarro.philology.transformer.SupportToolbarViewTransformer 11 | import com.jcminarro.philology.transformer.TextViewTransformer 12 | import com.jcminarro.philology.transformer.ToolbarViewTransformer 13 | import java.util.Locale 14 | 15 | object Philology { 16 | private val repositoryMap = mutableMapOf() 17 | private var factory: PhilologyRepositoryFactory = object : PhilologyRepositoryFactory { 18 | override fun getPhilologyRepository(locale: Locale): PhilologyRepository? = null 19 | } 20 | private var viewTransformerFactory: ViewTransformerFactory = emptyViewTransformerFactory 21 | 22 | @JvmOverloads 23 | fun init(factory: PhilologyRepositoryFactory, 24 | viewTransformerFactory: ViewTransformerFactory = emptyViewTransformerFactory) { 25 | this.factory = factory 26 | this.viewTransformerFactory = viewTransformerFactory 27 | repositoryMap.clear() 28 | } 29 | 30 | fun wrap(baseContext: Context): ContextWrapper = PhilologyContextWrapper(baseContext) 31 | 32 | internal fun getPhilologyRepository(locale: Locale): PhilologyRepository = 33 | repositoryMap[locale] ?: 34 | factory.getPhilologyRepository(locale)?.also {repositoryMap[locale] = it} ?: 35 | emptyPhilologyRepository 36 | 37 | internal fun getViewTransformer(view: View): ViewTransformer = 38 | viewTransformerFactory.getViewTransformer(view) ?: 39 | internalViewTransformerFactory.getViewTransformer(view) 40 | } 41 | 42 | interface PhilologyRepositoryFactory { 43 | fun getPhilologyRepository(locale: Locale): PhilologyRepository? 44 | } 45 | 46 | interface ViewTransformerFactory { 47 | fun getViewTransformer(view: View): ViewTransformer? 48 | } 49 | 50 | private val emptyPhilologyRepository = object : PhilologyRepository {} 51 | 52 | private val emptyViewTransformerFactory = object : ViewTransformerFactory { 53 | override fun getViewTransformer(view: View): ViewTransformer? = null 54 | } 55 | 56 | private val internalViewTransformerFactory = object : ViewTransformerFactory { 57 | override fun getViewTransformer(view: View): ViewTransformer = 58 | getNewApiViewTransformer(view) ?: 59 | getSupportedViewTransformer(view) ?: 60 | NoneViewTransformer 61 | 62 | private fun getSupportedViewTransformer(view: View): ViewTransformer? = when (view) { 63 | is Toolbar -> SupportToolbarViewTransformer 64 | is TextView -> TextViewTransformer 65 | else -> null 66 | } 67 | 68 | @SuppressWarnings("NewApi") 69 | private fun getNewApiViewTransformer(view: View): ViewTransformer? = 70 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 71 | null 72 | } else { 73 | when (view) { 74 | is android.widget.Toolbar -> ToolbarViewTransformer 75 | else -> null 76 | } 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Changed 9 | - Delegate Non-implemented methods of Resources into the baseResources by [Serghei Ceacal](https://github.com/antercepter) 10 | - Upgrade Kotlin to version 1.3.61 11 | - Upgrade Kluent to version 1.60 12 | - Upgrade Gradle to version 6.1.1 13 | 14 | ## [2.1.0] 15 | _2019-12-16_ [Github Diff](https://github.com/jcminarro/Philology/compare/v-2.0.1...v-2.1.0) 16 | ### Added 17 | - Support for texts attributes from style by [Ilya Ghirici](https://github.com/Ilya-Gh) 18 | - Improve how Text Resources Ids are read from attributes creating the new extension `Context.getStringResourceId()` by [Valerii Turcan](https://github.com/ffelini) 19 | 20 | ### Breaking changes 21 | - `ViewTransformer#setTextIfExists` has been removed 22 | 23 | ## [2.0.1] 24 | _2019-11-07_ [Github Diff](https://github.com/jcminarro/Philology/compare/v-2.0.0...v-2.0.1) 25 | ### Added 26 | - Add `Resources#getText(id: Int, def: CharSequence)` support 27 | 28 | ## [2.0.0] 29 | _2019-09-03_ [Github Diff](https://github.com/jcminarro/Philology/compare/v-1.1.2...v-2.0.0) 30 | ### Added 31 | - AppCompact from AndroidX Project 32 | 33 | ### Changed 34 | - Migrate to AndroidX 35 | - Upgrade ViewPump to version 2.0.3 36 | 37 | ### Removed 38 | - AppCompact from the old Android Support library 39 | 40 | ## [1.1.2] 41 | _2019-09-03_ [Github Diff](https://github.com/jcminarro/Philology/compare/v-1.1.1...v-1.1.2) 42 | 43 | ### Added 44 | - A `CHANGELOG.md` File to track every new change on the library by [Jc Miñarro](https://github.com/jcminarro) 45 | - Add String Array Support by [Maxim Bircu](https://github.com/mbircu-ellation) 46 | 47 | ## [1.1.1] 48 | _2019-08-08_ [Github Diff](https://github.com/jcminarro/Philology/compare/v-1.1.0...v-1.1.1) 49 | 50 | ### Added 51 | - Custom implementation to support Plurals on Old Android API Versions by [Serghei Oleinicenco](https://github.com/pr0t3us) 52 | 53 | ### Removed 54 | - `com.ibm.icu:icu4j` Dependency 55 | 56 | ## [1.1.0] 57 | _2019-07-18_ [Github Diff](https://github.com/jcminarro/Philology/compare/v-1.0.1...v-1.1.0) 58 | 59 | ### Added 60 | - Android Plurals support by [Serghei Oleinicenco](https://github.com/pr0t3us) 61 | - `com.ibm.icu:icu4j` Dependency to support plural on Old Android API Versions 62 | 63 | ## [1.0.1] 64 | _2018-08-04_ [Github Diff](https://github.com/jcminarro/Philology/compare/v-1.0.0...v-1.0.1) 65 | 66 | ### Added 67 | - Robolectric framework to test cross multiple Android API Versions by [Jc Miñarro](https://github.com/jcminarro) 68 | 69 | ### Fixed 70 | - Crash when checking if a view is Toolbar in Android version lower than Lolipop by [Jc Miñarro](https://github.com/jcminarro) 71 | 72 | ## [1.0.0] 73 | _2018-07-05_ Initial version 74 | 75 | [Unreleased]: https://github.com/jcminarro/Philology/compare/v-2.1.0...HEAD 76 | [2.1.0]: https://github.com/jcminarro/Philology/releases/tag/v-2.1.0 77 | [2.0.1]: https://github.com/jcminarro/Philology/releases/tag/v-2.0.1 78 | [2.0.0]: https://github.com/jcminarro/Philology/releases/tag/v-2.0.0 79 | [1.1.2]: https://github.com/jcminarro/Philology/releases/tag/v-1.1.2 80 | [1.1.1]: https://github.com/jcminarro/Philology/releases/tag/v-1.1.1 81 | [1.1.0]: https://github.com/jcminarro/Philology/releases/tag/v-1.1.0 82 | [1.0.1]: https://github.com/jcminarro/Philology/releases/tag/v-1.0.1 83 | [1.0.0]: https://github.com/jcminarro/Philology/releases/tag/v-1.0.0 -------------------------------------------------------------------------------- /philology/src/test/java/com/jcminarro/philology/transformer/TextViewTransformerTest.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology.transformer 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.widget.TextView 6 | import com.jcminarro.philology.ResourceIdAttribute.HintAttribute 7 | import com.jcminarro.philology.ResourceIdAttribute.TextAttribute 8 | import com.jcminarro.philology.createAttributeSet 9 | import com.nhaarman.mockito_kotlin.doReturn 10 | import org.amshove.kluent.Verify 11 | import org.amshove.kluent.VerifyNotCalled 12 | import org.amshove.kluent.When 13 | import org.amshove.kluent.`Verify no further interactions` 14 | import org.amshove.kluent.`should be` 15 | import org.amshove.kluent.called 16 | import org.amshove.kluent.calling 17 | import org.amshove.kluent.mock 18 | import org.amshove.kluent.on 19 | import org.amshove.kluent.that 20 | import org.amshove.kluent.was 21 | import org.junit.Before 22 | import org.junit.Test 23 | import org.mockito.ArgumentMatchers.anyInt 24 | 25 | private const val TEXT_RES_ID = 1039982 26 | private const val HINT_RES_ID = 3879817 27 | private const val INVALID_RES_ID = -1 28 | 29 | class TextViewTransformerTest { 30 | 31 | private val view: View = mock() 32 | private val textView: TextView = mock() 33 | private val context: Context = mock() 34 | 35 | @Before 36 | fun setUp() { 37 | When calling textView.context doReturn context 38 | } 39 | 40 | @Test 41 | fun `View should be the same`() { 42 | TextViewTransformer.reword(view, textView.createAttributeSet()) `should be` view 43 | } 44 | 45 | @Test 46 | fun `Inflated textView should be the same`() { 47 | TextViewTransformer.reword( 48 | textView, textView.createAttributeSet( 49 | TextAttribute(INVALID_RES_ID), 50 | HintAttribute(INVALID_RES_ID) 51 | ) 52 | ) `should be` textView 53 | } 54 | 55 | @Test 56 | fun `View shouldn't be modified`() { 57 | TextViewTransformer.reword(view, textView.createAttributeSet()) 58 | 59 | `Verify no further interactions` on view 60 | } 61 | 62 | @Test 63 | fun `Should reword only the text`() { 64 | val viewResult = TextViewTransformer.reword( 65 | textView, textView.createAttributeSet( 66 | TextAttribute(TEXT_RES_ID), 67 | HintAttribute(INVALID_RES_ID) 68 | ) 69 | ) 70 | 71 | viewResult `should be` textView 72 | Verify on textView that textView.setText(TEXT_RES_ID) was called 73 | VerifyNotCalled on textView that textView.setHint(anyInt()) 74 | } 75 | 76 | @Test 77 | fun `Should reword only the hint`() { 78 | val viewResult = TextViewTransformer.reword( 79 | textView, textView.createAttributeSet( 80 | TextAttribute(INVALID_RES_ID), 81 | HintAttribute(HINT_RES_ID) 82 | ) 83 | ) 84 | 85 | viewResult `should be` textView 86 | Verify on textView that textView.setHint(HINT_RES_ID) was called 87 | VerifyNotCalled on textView that textView.setText(anyInt()) 88 | } 89 | 90 | @Test 91 | fun `Should reword text and hint`() { 92 | val viewResult = TextViewTransformer.reword( 93 | textView, textView.createAttributeSet( 94 | TextAttribute(TEXT_RES_ID), 95 | HintAttribute(HINT_RES_ID) 96 | ) 97 | ) 98 | 99 | viewResult `should be` textView 100 | Verify on textView that textView.setText(TEXT_RES_ID) was called 101 | Verify on textView that textView.setHint(HINT_RES_ID) was called 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | working_directory: ~/code 3 | docker: 4 | - image: circleci/android:api-28 5 | environment: 6 | TZ: Europe/Madrid 7 | JVM_OPTS: -Xmx3200m 8 | GRADLE_OPTS: '-Dorg.gradle.daemon=false' 9 | _JAVA_OPTIONS: "-Xms256m -Xmx1280m -XX:MaxPermSize=350m" 10 | 11 | configure_gradle: &configure_gradle 12 | name: Configure Gradle Properties 13 | command: | 14 | mkdir -p ~/.gradle 15 | touch ~/.gradle/gradle.properties 16 | echo "org.gradle.daemon=false" > ~/.gradle/gradle.properties 17 | echo "org.gradle.jvmargs=-Xmx1536m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError" >> ~/.gradle/gradle.properties 18 | download_gradle_dependencies: &download_gradle_dependencies 19 | name: Download dependencies 20 | command: ./gradlew prefetchDependencies dependencies :philology:dependencies sample:dependencies 21 | restore_cache: &restore_cache 22 | key: jars--{{ checksum "build.gradle" }}-{{ checksum "philology/build.gradle" }}-{{ checksum "sample/build.gradle" }}-{{ checksum "buildSrc/src/main/groovy/com/jcminarro/Dependencies.groovy" }} 23 | save_cache: &save_cache 24 | paths: 25 | - ~/.gradle 26 | key: jars--{{ checksum "build.gradle" }}-{{ checksum "philology/build.gradle" }}-{{ checksum "sample/build.gradle" }}-{{ checksum "buildSrc/src/main/groovy/com/jcminarro/Dependencies.groovy" }} 27 | 28 | version: 2 29 | jobs: 30 | build_philology: 31 | <<: *defaults 32 | steps: 33 | - checkout 34 | - run: *configure_gradle 35 | - restore_cache: *restore_cache 36 | - run: *download_gradle_dependencies 37 | - save_cache: *save_cache 38 | - run: 39 | name: Build Philology lib 40 | command: ./gradlew philology:assemble --stacktrace -PdisablePreDex 41 | - store_artifacts: 42 | path: philology/build/outputs/ 43 | destination: artifacts 44 | - persist_to_workspace: 45 | root: . 46 | paths: 47 | - buildSrc/build 48 | - build 49 | - philology/build 50 | 51 | test_philology: 52 | <<: *defaults 53 | steps: 54 | - checkout 55 | - run: *configure_gradle 56 | - restore_cache: *restore_cache 57 | - run: *download_gradle_dependencies 58 | - save_cache: *save_cache 59 | - attach_workspace: 60 | at: . 61 | - run: 62 | name: Test Philology lib 63 | command: ./gradlew philology:test --stacktrace -PdisablePreDex 64 | - store_artifacts: 65 | path: philology/build/reports 66 | destination: reports 67 | - store_test_results: 68 | path: philology/build/test-results 69 | 70 | lint_philology: 71 | <<: *defaults 72 | steps: 73 | - checkout 74 | - run: *configure_gradle 75 | - restore_cache: *restore_cache 76 | - run: *download_gradle_dependencies 77 | - save_cache: *save_cache 78 | - attach_workspace: 79 | at: . 80 | - run: 81 | name: Run lint Philology lib 82 | command: ./gradlew philology:lint --stacktrace -PdisablePreDex 83 | - store_artifacts: 84 | path: philology/build/reports 85 | destination: reports 86 | 87 | build_sample: 88 | <<: *defaults 89 | steps: 90 | - checkout 91 | - run: *configure_gradle 92 | - restore_cache: *restore_cache 93 | - run: *download_gradle_dependencies 94 | - save_cache: *save_cache 95 | - attach_workspace: 96 | at: . 97 | - run: 98 | name: Build Philology sample 99 | command: ./gradlew sample:assemble --stacktrace -PdisablePreDex 100 | - store_artifacts: 101 | path: sample/build/outputs/apk/ 102 | destination: apks 103 | 104 | deploy_philology: 105 | <<: *defaults 106 | steps: 107 | - checkout 108 | - run: *configure_gradle 109 | - restore_cache: *restore_cache 110 | - run: *download_gradle_dependencies 111 | - save_cache: *save_cache 112 | - attach_workspace: 113 | at: . 114 | - run: 115 | name: Deploy Philology lib 116 | command: ./gradlew philology:uploadArchives -PBINTRAY_API_KEY=$BINTRAY_API_KEY -PBINTRAY_USER=$BINTRAY_USER --stacktrace -PdisablePreDex 117 | - store_artifacts: 118 | path: philology/build/reports 119 | destination: reports 120 | - store_test_results: 121 | path: philology/build/test-results 122 | 123 | workflows: 124 | version: 2 125 | test_and_build: 126 | jobs: 127 | - build_philology 128 | - test_philology: 129 | requires: 130 | - build_philology 131 | - lint_philology: 132 | requires: 133 | - build_philology 134 | - build_sample: 135 | requires: 136 | - build_philology 137 | - deploy_philology: 138 | requires: 139 | - lint_philology 140 | - test_philology 141 | filters: 142 | branches: 143 | only: 144 | - master -------------------------------------------------------------------------------- /philology/src/test/java/com/jcminarro/philology/Mother.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology 2 | 3 | import android.content.Context 4 | import android.content.res.Configuration 5 | import android.content.res.Resources 6 | import android.content.res.TypedArray 7 | import android.os.LocaleList 8 | import android.util.AttributeSet 9 | import android.view.View 10 | import androidx.annotation.AttrRes 11 | import com.nhaarman.mockito_kotlin.doReturn 12 | import com.nhaarman.mockito_kotlin.doThrow 13 | import org.amshove.kluent.When 14 | import org.amshove.kluent.calling 15 | import org.amshove.kluent.mock 16 | import java.util.Locale 17 | 18 | fun createConfiguration(locale: Locale = Locale.ENGLISH): Configuration = mock().apply { 19 | @Suppress("DEPRECATION") 20 | this.locale = locale 21 | When calling this.locales doReturn LocaleList(locale) 22 | } 23 | 24 | fun configureResourceGetText( 25 | resources: Resources, 26 | id: Int, 27 | nameId: String, 28 | text: CharSequence 29 | ) { 30 | When calling resources.getResourceEntryName(id) doReturn nameId 31 | When calling resources.getText(id) doReturn text 32 | } 33 | 34 | fun configureResourceQuantityString(resources: Resources, quantity: Int, quantityString: String) { 35 | When calling resources.getQuantityString( 36 | R.plurals.com_jcminarro_philology_quantity_string, 37 | quantity 38 | ) doReturn quantityString 39 | } 40 | 41 | fun configureResourceGetIdException(resources: Resources, id: Int) { 42 | When calling resources.getResourceEntryName(id) doThrow Resources.NotFoundException() 43 | } 44 | 45 | fun configureResourceGetQuantityText( 46 | resources: Resources, id: Int, nameId: String, quantity: Int, text: CharSequence 47 | ) { 48 | When calling resources.getResourceEntryName(id) doReturn nameId 49 | When calling resources.getQuantityText(id, quantity) doReturn text 50 | } 51 | 52 | fun configureResourceGetTextArray( 53 | resources: Resources, id: Int, nameId: String, textArray: Array 54 | ) { 55 | When calling resources.getResourceEntryName(id) doReturn nameId 56 | When calling resources.getTextArray(id) doReturn textArray 57 | } 58 | 59 | fun clearPhilology() { 60 | Philology.init(object : PhilologyRepositoryFactory { 61 | override fun getPhilologyRepository(locale: Locale): PhilologyRepository? = null 62 | }) 63 | } 64 | 65 | fun createRepository( 66 | nameId: String, 67 | quantity: String? = null, 68 | text: CharSequence? = null, 69 | textArray: Array? = null 70 | ): PhilologyRepository = 71 | object : PhilologyRepository { 72 | override fun getText(key: String) = text.takeIf { key == nameId } 73 | 74 | override fun getPlural(key: String, quantityString: String): CharSequence? { 75 | return text.takeIf { key == nameId && quantity == quantity } 76 | } 77 | 78 | override fun getTextArray(key: String) = textArray.takeIf { key == nameId } 79 | } 80 | 81 | fun createFactory(vararg repositoryPairs: Pair): PhilologyRepositoryFactory = 82 | object : PhilologyRepositoryFactory { 83 | override fun getPhilologyRepository(locale: Locale): PhilologyRepository? = 84 | repositoryPairs.firstOrNull { it.first == locale }?.second 85 | } 86 | 87 | fun configurePhilology(repository: PhilologyRepository, locale: Locale = Locale.ENGLISH) { 88 | Philology.init(createFactory(locale to repository)) 89 | } 90 | 91 | fun View.createAttributeSet(vararg attributeType: ResourceIdAttribute): AttributeSet { 92 | val context = mock() 93 | When calling this.context doReturn context 94 | return mock().apply { 95 | attributeType.forEachIndexed { _, at -> 96 | val typedArray = mock() 97 | When calling typedArray.getResourceId(0, -1) doReturn at.stringResId 98 | When calling context.obtainStyledAttributes( 99 | this, 100 | intArrayOf(at.attrResId) 101 | ) doReturn typedArray 102 | } 103 | } 104 | } 105 | 106 | sealed class ResourceIdAttribute(@AttrRes val attrResId: Int, open val stringResId: Int) { 107 | data class TextAttribute(override val stringResId: Int) : 108 | ResourceIdAttribute(android.R.attr.text, stringResId) 109 | 110 | data class HintAttribute(override val stringResId: Int) : 111 | ResourceIdAttribute(android.R.attr.hint, stringResId) 112 | 113 | data class TitleAttribute(override val stringResId: Int) : 114 | ResourceIdAttribute(android.R.attr.title, stringResId) 115 | 116 | data class CompatTitleAttribute(override val stringResId: Int) : 117 | ResourceIdAttribute(androidx.appcompat.R.attr.title, stringResId) 118 | 119 | data class SubtitleAttribute(override val stringResId: Int) : 120 | ResourceIdAttribute(android.R.attr.subtitle, stringResId) 121 | 122 | data class CompatSubtitleAttribute(override val stringResId: Int) : 123 | ResourceIdAttribute(androidx.appcompat.R.attr.subtitle, stringResId) 124 | } 125 | -------------------------------------------------------------------------------- /sample/src/main/java/com/jcminarro/philology/sample/App.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology.sample 2 | 3 | import android.app.Application 4 | import com.jcminarro.philology.Philology 5 | import com.jcminarro.philology.PhilologyInterceptor 6 | import com.jcminarro.philology.PhilologyRepository 7 | import com.jcminarro.philology.PhilologyRepositoryFactory 8 | import io.github.inflationx.viewpump.ViewPump 9 | import java.util.Locale 10 | 11 | class App : Application() { 12 | 13 | override fun onCreate() { 14 | super.onCreate() 15 | // Init Philology with our PhilologyRepositoryFactory 16 | Philology.init(MyPhilologyRepositoryFactory) 17 | // Add PhilologyInterceptor to ViewPump 18 | // If you are already using Calligraphy you can add both interceptors, there is no problem 19 | ViewPump.init(ViewPump.builder().addInterceptor(PhilologyInterceptor).build()) 20 | } 21 | } 22 | 23 | object MyPhilologyRepositoryFactory : PhilologyRepositoryFactory { 24 | override fun getPhilologyRepository(locale: Locale): PhilologyRepository? = when (locale) { 25 | Locale.ENGLISH -> EnglishPhilologyRepository 26 | Locale("es", "ES") -> SpanishPhilologyRepository 27 | Locale("ru", "RU") -> RussianPhilologyRepository 28 | // If we don't support a language we could return null as PhilologyRepository and 29 | // values from the strings resources file will be used 30 | else -> null 31 | } 32 | } 33 | 34 | object EnglishPhilologyRepository : PhilologyRepository { 35 | private val pluralsSample = mapOf("one" to "test one", "other" to "test other") 36 | private val pluralsSampleFormat = mapOf("one" to "%s test one", "other" to "%s tests") 37 | private val resourceSample = mapOf( 38 | "plurals_sample" to pluralsSample, 39 | "plurals_sample_format" to pluralsSampleFormat 40 | ) 41 | 42 | override fun getText(key: String): CharSequence? = when (key) { 43 | "label" -> "New value for the `label` key, it could be fetched from a database or an external API server" 44 | else -> null 45 | } 46 | 47 | override fun getPlural(key: String, quantityString: String): CharSequence? { 48 | return resourceSample[key]?.get(quantityString) 49 | } 50 | } 51 | 52 | object SpanishPhilologyRepository : PhilologyRepository { 53 | private val pluralsSample = mapOf("one" to "prueba uno", "other" to "prueba otro") 54 | private val pluralsSampleFormat = mapOf("one" to "prueba uno %s", "other" to "prueba otro %s") 55 | private val resourceSample = mapOf( 56 | "plurals_sample" to pluralsSample, 57 | "plurals_sample_format" to pluralsSampleFormat 58 | ) 59 | 60 | override fun getText(key: String): CharSequence? = when (key) { 61 | "label" -> "Nuevo valor para la clave `label`, puede ser obtenida de una base de datos o un servidor externo" 62 | "toolbar_title" -> "Philology muestra" 63 | "plural_quantity_hint" -> "Cantidad plural editar" 64 | "plural_quantity_format_hint" -> "Formato de edición de cantidad plural" 65 | else -> null 66 | } 67 | 68 | override fun getPlural(key: String, quantityString: String): CharSequence? { 69 | return resourceSample[key]?.get(quantityString) 70 | } 71 | 72 | override fun getTextArray(key: String) = when (key) { 73 | "days" -> arrayOf( 74 | "lunes", 75 | "martes", 76 | "miércoles", 77 | "jueves", 78 | "viernes", 79 | "sábado", 80 | "domingo" 81 | ) 82 | else -> null 83 | } 84 | } 85 | 86 | object RussianPhilologyRepository : PhilologyRepository { 87 | private val pluralsSample = mapOf( 88 | "one" to "один тест", 89 | "other" to "другие тесты", 90 | "few" to "несколько тестов", 91 | "many" to "много тестов" 92 | ) 93 | private val pluralsSampleFormat = mapOf( 94 | "one" to "%s тест", 95 | "other" to "%s тесты", 96 | "few" to "%s теста", 97 | "many" to "%s тестов" 98 | ) 99 | private val resourceSample = mapOf( 100 | "plurals_sample" to pluralsSample, 101 | "plurals_sample_format" to pluralsSampleFormat 102 | ) 103 | 104 | override fun getText(key: String): CharSequence? = when (key) { 105 | "label" -> "Новое значение для ключа `label`, его можно получить из базы данных или внешнего сервера API" 106 | "toolbar_title" -> "Philology Пример" 107 | "plural_quantity_hint" -> "Редактировать множественное количество" 108 | "plural_quantity_format_hint" -> "Формат редактирования множественного числа" 109 | else -> null 110 | } 111 | 112 | override fun getPlural(key: String, quantityString: String): CharSequence? { 113 | return resourceSample[key]?.get(quantityString) 114 | } 115 | 116 | override fun getTextArray(key: String) = when (key) { 117 | "days" -> arrayOf( 118 | "понедельник", 119 | "вторник", 120 | "среда", 121 | "четверг", 122 | "пятница", 123 | "суббота", 124 | "воскресенье" 125 | ) 126 | else -> null 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 14 | 15 | 24 | 25 | 36 | 37 | 48 | 49 | 60 | 61 | 71 | 72 | 83 | 84 | 94 | 95 | 110 | 111 | 124 | 125 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /philology/src/test/java/com/jcminarro/philology/transformer/ToolbarViewTransformerTest.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology.transformer 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.widget.Toolbar 6 | import com.jcminarro.philology.ResourceIdAttribute.CompatSubtitleAttribute 7 | import com.jcminarro.philology.ResourceIdAttribute.CompatTitleAttribute 8 | import com.jcminarro.philology.ResourceIdAttribute.SubtitleAttribute 9 | import com.jcminarro.philology.ResourceIdAttribute.TitleAttribute 10 | import com.jcminarro.philology.createAttributeSet 11 | import com.nhaarman.mockito_kotlin.doReturn 12 | import org.amshove.kluent.Verify 13 | import org.amshove.kluent.VerifyNotCalled 14 | import org.amshove.kluent.When 15 | import org.amshove.kluent.`Verify no further interactions` 16 | import org.amshove.kluent.`should be` 17 | import org.amshove.kluent.called 18 | import org.amshove.kluent.calling 19 | import org.amshove.kluent.mock 20 | import org.amshove.kluent.on 21 | import org.amshove.kluent.that 22 | import org.amshove.kluent.was 23 | import org.junit.Before 24 | import org.junit.Test 25 | import org.mockito.ArgumentMatchers.anyInt 26 | 27 | private const val TITLE_RES_ID = 1039982 28 | private const val SUBTITLE_RES_ID = 3879817 29 | private const val INVALID_RES_ID = -1 30 | 31 | class ToolbarViewTransformerTest { 32 | 33 | private val view: View = mock() 34 | private val toolbar: Toolbar = mock() 35 | private val context: Context = mock() 36 | 37 | @Before 38 | fun setUp() { 39 | When calling toolbar.context doReturn context 40 | } 41 | 42 | @Test 43 | fun `View should be the same`() { 44 | ToolbarViewTransformer.reword(view, toolbar.createAttributeSet()) `should be` view 45 | } 46 | 47 | @Test 48 | fun `Inflated toolbar should be the same`() { 49 | ToolbarViewTransformer.reword( 50 | toolbar, toolbar.createAttributeSet( 51 | TitleAttribute(TITLE_RES_ID), 52 | SubtitleAttribute(INVALID_RES_ID), 53 | CompatTitleAttribute(INVALID_RES_ID), 54 | CompatSubtitleAttribute(INVALID_RES_ID) 55 | ) 56 | ) `should be` toolbar 57 | } 58 | 59 | @Test 60 | fun `View shouldn't be modified`() { 61 | ToolbarViewTransformer.reword(view, toolbar.createAttributeSet()) 62 | 63 | `Verify no further interactions` on view 64 | } 65 | 66 | @Test 67 | fun `Should reword only the title`() { 68 | val viewResult = ToolbarViewTransformer.reword( 69 | toolbar, toolbar.createAttributeSet( 70 | TitleAttribute(TITLE_RES_ID), 71 | SubtitleAttribute(INVALID_RES_ID), 72 | CompatTitleAttribute(INVALID_RES_ID), 73 | CompatSubtitleAttribute(INVALID_RES_ID) 74 | ) 75 | ) 76 | 77 | viewResult `should be` toolbar 78 | Verify on toolbar that toolbar.setTitle(TITLE_RES_ID) was called 79 | VerifyNotCalled on toolbar that toolbar.setSubtitle(anyInt()) 80 | } 81 | 82 | @Test 83 | fun `Should reword only the compat title`() { 84 | val viewResult = ToolbarViewTransformer.reword( 85 | toolbar, toolbar.createAttributeSet( 86 | CompatTitleAttribute(TITLE_RES_ID), 87 | CompatSubtitleAttribute(INVALID_RES_ID), 88 | TitleAttribute(INVALID_RES_ID), 89 | SubtitleAttribute(INVALID_RES_ID) 90 | ) 91 | ) 92 | 93 | viewResult `should be` toolbar 94 | Verify on toolbar that toolbar.setTitle(TITLE_RES_ID) was called 95 | VerifyNotCalled on toolbar that toolbar.setSubtitle(anyInt()) 96 | } 97 | 98 | @Test 99 | fun `Should reword only the subtitle`() { 100 | val viewResult = ToolbarViewTransformer.reword( 101 | toolbar, toolbar.createAttributeSet( 102 | SubtitleAttribute(SUBTITLE_RES_ID), 103 | TitleAttribute(INVALID_RES_ID), 104 | CompatTitleAttribute(INVALID_RES_ID), 105 | CompatSubtitleAttribute(INVALID_RES_ID) 106 | ) 107 | ) 108 | 109 | viewResult `should be` toolbar 110 | Verify on toolbar that toolbar.setSubtitle(SUBTITLE_RES_ID) was called 111 | VerifyNotCalled on toolbar that toolbar.setTitle(anyInt()) 112 | } 113 | 114 | @Test 115 | fun `Should reword only the compat subtitle`() { 116 | val viewResult = ToolbarViewTransformer.reword( 117 | toolbar, toolbar.createAttributeSet( 118 | CompatSubtitleAttribute(SUBTITLE_RES_ID), 119 | CompatTitleAttribute(INVALID_RES_ID), 120 | SubtitleAttribute(INVALID_RES_ID), 121 | TitleAttribute(INVALID_RES_ID) 122 | ) 123 | ) 124 | 125 | viewResult `should be` toolbar 126 | Verify on toolbar that toolbar.setSubtitle(SUBTITLE_RES_ID) was called 127 | VerifyNotCalled on toolbar that toolbar.setTitle(anyInt()) 128 | } 129 | 130 | @Test 131 | fun `Should reword title and subtitle`() { 132 | val viewResult = ToolbarViewTransformer.reword( 133 | toolbar, toolbar.createAttributeSet( 134 | TitleAttribute(TITLE_RES_ID), 135 | SubtitleAttribute(SUBTITLE_RES_ID), 136 | CompatTitleAttribute(INVALID_RES_ID), 137 | CompatSubtitleAttribute(INVALID_RES_ID) 138 | ) 139 | ) 140 | 141 | viewResult `should be` toolbar 142 | Verify on toolbar that toolbar.setTitle(TITLE_RES_ID) was called 143 | Verify on toolbar that toolbar.setSubtitle(SUBTITLE_RES_ID) was called 144 | } 145 | 146 | @Test 147 | fun `Should reword compat title and subtitle`() { 148 | val viewResult = ToolbarViewTransformer.reword( 149 | toolbar, toolbar.createAttributeSet( 150 | CompatTitleAttribute(TITLE_RES_ID), 151 | CompatSubtitleAttribute(SUBTITLE_RES_ID), 152 | TitleAttribute(INVALID_RES_ID), 153 | SubtitleAttribute(INVALID_RES_ID) 154 | ) 155 | ) 156 | 157 | viewResult `should be` toolbar 158 | Verify on toolbar that toolbar.setTitle(TITLE_RES_ID) was called 159 | Verify on toolbar that toolbar.setSubtitle(SUBTITLE_RES_ID) was called 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /philology/src/test/java/com/jcminarro/philology/transformer/SupportToolbarViewTransformerTest.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology.transformer 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import androidx.appcompat.widget.Toolbar 6 | import com.jcminarro.philology.ResourceIdAttribute.CompatSubtitleAttribute 7 | import com.jcminarro.philology.ResourceIdAttribute.CompatTitleAttribute 8 | import com.jcminarro.philology.ResourceIdAttribute.SubtitleAttribute 9 | import com.jcminarro.philology.ResourceIdAttribute.TitleAttribute 10 | import com.jcminarro.philology.createAttributeSet 11 | import com.nhaarman.mockito_kotlin.doReturn 12 | import org.amshove.kluent.Verify 13 | import org.amshove.kluent.VerifyNotCalled 14 | import org.amshove.kluent.When 15 | import org.amshove.kluent.`Verify no further interactions` 16 | import org.amshove.kluent.`should be` 17 | import org.amshove.kluent.called 18 | import org.amshove.kluent.calling 19 | import org.amshove.kluent.mock 20 | import org.amshove.kluent.on 21 | import org.amshove.kluent.that 22 | import org.amshove.kluent.was 23 | import org.junit.Before 24 | import org.junit.Test 25 | import org.mockito.ArgumentMatchers.anyInt 26 | 27 | private const val TITLE_RES_ID = 1039982 28 | private const val SUBTITLE_RES_ID = 3879817 29 | private const val INVALID_RES_ID = -1 30 | 31 | class SupportToolbarViewTransformerTest { 32 | 33 | private val view: View = mock() 34 | private val toolbar: Toolbar = mock() 35 | private val context: Context = mock() 36 | 37 | @Before 38 | fun setUp() { 39 | When calling toolbar.context doReturn context 40 | } 41 | 42 | @Test 43 | fun `View should be the same`() { 44 | SupportToolbarViewTransformer.reword(view, toolbar.createAttributeSet()) `should be` view 45 | } 46 | 47 | @Test 48 | fun `Inflated toolbar should be the same`() { 49 | SupportToolbarViewTransformer.reword( 50 | toolbar, toolbar.createAttributeSet( 51 | TitleAttribute(TITLE_RES_ID), 52 | SubtitleAttribute(INVALID_RES_ID), 53 | CompatTitleAttribute(INVALID_RES_ID), 54 | CompatSubtitleAttribute(INVALID_RES_ID) 55 | ) 56 | ) `should be` toolbar 57 | } 58 | 59 | @Test 60 | fun `View shouldn't be modified`() { 61 | SupportToolbarViewTransformer.reword(view, toolbar.createAttributeSet()) 62 | 63 | `Verify no further interactions` on view 64 | } 65 | 66 | @Test 67 | fun `Should reword only the title`() { 68 | val viewResult = SupportToolbarViewTransformer.reword( 69 | toolbar, toolbar.createAttributeSet( 70 | TitleAttribute(TITLE_RES_ID), 71 | SubtitleAttribute(INVALID_RES_ID), 72 | CompatTitleAttribute(INVALID_RES_ID), 73 | CompatSubtitleAttribute(INVALID_RES_ID) 74 | ) 75 | ) 76 | 77 | viewResult `should be` toolbar 78 | Verify on toolbar that toolbar.setTitle(TITLE_RES_ID) was called 79 | VerifyNotCalled on toolbar that toolbar.setSubtitle(anyInt()) 80 | } 81 | 82 | @Test 83 | fun `Should reword only the compat title`() { 84 | val viewResult = SupportToolbarViewTransformer.reword( 85 | toolbar, toolbar.createAttributeSet( 86 | CompatTitleAttribute(TITLE_RES_ID), 87 | CompatSubtitleAttribute(INVALID_RES_ID), 88 | TitleAttribute(INVALID_RES_ID), 89 | SubtitleAttribute(INVALID_RES_ID) 90 | ) 91 | ) 92 | 93 | viewResult `should be` toolbar 94 | Verify on toolbar that toolbar.setTitle(TITLE_RES_ID) was called 95 | VerifyNotCalled on toolbar that toolbar.setSubtitle(anyInt()) 96 | } 97 | 98 | @Test 99 | fun `Should reword only the subtitle`() { 100 | val viewResult = SupportToolbarViewTransformer.reword( 101 | toolbar, toolbar.createAttributeSet( 102 | SubtitleAttribute(SUBTITLE_RES_ID), 103 | TitleAttribute(INVALID_RES_ID), 104 | CompatTitleAttribute(INVALID_RES_ID), 105 | CompatSubtitleAttribute(INVALID_RES_ID) 106 | ) 107 | ) 108 | 109 | viewResult `should be` toolbar 110 | Verify on toolbar that toolbar.setSubtitle(SUBTITLE_RES_ID) was called 111 | VerifyNotCalled on toolbar that toolbar.setTitle(anyInt()) 112 | } 113 | 114 | @Test 115 | fun `Should reword only the compat subtitle`() { 116 | val viewResult = SupportToolbarViewTransformer.reword( 117 | toolbar, toolbar.createAttributeSet( 118 | CompatSubtitleAttribute(SUBTITLE_RES_ID), 119 | CompatTitleAttribute(INVALID_RES_ID), 120 | SubtitleAttribute(INVALID_RES_ID), 121 | TitleAttribute(INVALID_RES_ID) 122 | ) 123 | ) 124 | 125 | viewResult `should be` toolbar 126 | Verify on toolbar that toolbar.setSubtitle(SUBTITLE_RES_ID) was called 127 | VerifyNotCalled on toolbar that toolbar.setTitle(anyInt()) 128 | } 129 | 130 | @Test 131 | fun `Should reword title and subtitle`() { 132 | val viewResult = SupportToolbarViewTransformer.reword( 133 | toolbar, toolbar.createAttributeSet( 134 | TitleAttribute(TITLE_RES_ID), 135 | SubtitleAttribute(SUBTITLE_RES_ID), 136 | CompatTitleAttribute(INVALID_RES_ID), 137 | CompatSubtitleAttribute(INVALID_RES_ID) 138 | ) 139 | ) 140 | 141 | viewResult `should be` toolbar 142 | Verify on toolbar that toolbar.setTitle(TITLE_RES_ID) was called 143 | Verify on toolbar that toolbar.setSubtitle(SUBTITLE_RES_ID) was called 144 | } 145 | 146 | @Test 147 | fun `Should reword compat title and subtitle`() { 148 | val viewResult = SupportToolbarViewTransformer.reword( 149 | toolbar, toolbar.createAttributeSet( 150 | CompatTitleAttribute(TITLE_RES_ID), 151 | CompatSubtitleAttribute(SUBTITLE_RES_ID), 152 | TitleAttribute(INVALID_RES_ID), 153 | SubtitleAttribute(INVALID_RES_ID) 154 | ) 155 | ) 156 | 157 | viewResult `should be` toolbar 158 | Verify on toolbar that toolbar.setTitle(TITLE_RES_ID) was called 159 | Verify on toolbar that toolbar.setSubtitle(SUBTITLE_RES_ID) was called 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 13 | 19 | 25 | 31 | 37 | 43 | 49 | 55 | 61 | 67 | 73 | 79 | 85 | 91 | 97 | 103 | 109 | 115 | 121 | 127 | 133 | 139 | 145 | 151 | 157 | 163 | 169 | 175 | 181 | 187 | 193 | 199 | 205 | 206 | -------------------------------------------------------------------------------- /philology/src/main/java/com/jcminarro/philology/PhilologyResources.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology 2 | 3 | import android.annotation.SuppressLint 4 | import android.annotation.TargetApi 5 | import android.content.res.AssetFileDescriptor 6 | import android.content.res.ColorStateList 7 | import android.content.res.Configuration 8 | import android.content.res.Resources 9 | import android.content.res.TypedArray 10 | import android.content.res.XmlResourceParser 11 | import android.graphics.Movie 12 | import android.graphics.Typeface 13 | import android.graphics.drawable.Drawable 14 | import android.os.Build.VERSION_CODES 15 | import android.os.Bundle 16 | import android.util.AttributeSet 17 | import android.util.DisplayMetrics 18 | import android.util.TypedValue 19 | import androidx.annotation.RequiresApi 20 | import org.xmlpull.v1.XmlPullParserException 21 | import java.io.IOException 22 | import java.io.InputStream 23 | 24 | @Suppress("DEPRECATION") 25 | internal class PhilologyResources( 26 | private val baseResources: Resources 27 | ) : Resources(baseResources.assets, baseResources.displayMetrics, baseResources.configuration) { 28 | private val resourcesUtil = ResourcesUtil(baseResources) 29 | 30 | override fun getText(id: Int): CharSequence = resourcesUtil.getText(id) 31 | 32 | override fun getText(id: Int, def: CharSequence): CharSequence = try { 33 | getText(id) 34 | } catch (_: NotFoundException) { 35 | def 36 | } 37 | 38 | override fun getString(id: Int): String = resourcesUtil.getString(id) 39 | 40 | override fun getQuantityText(id: Int, quantity: Int): CharSequence = 41 | resourcesUtil.getQuantityText(id, quantity) 42 | 43 | override fun getQuantityString(id: Int, quantity: Int): String = 44 | resourcesUtil.getQuantityString(id, quantity) 45 | 46 | override fun getQuantityString(id: Int, quantity: Int, vararg formatArgs: Any?): String = 47 | resourcesUtil.getQuantityString(id, quantity, *formatArgs) 48 | 49 | override fun getStringArray(id: Int): Array = resourcesUtil.getStringArray(id) 50 | 51 | override fun getTextArray(id: Int): Array = resourcesUtil.getTextArray(id) 52 | 53 | @Throws(NotFoundException::class) 54 | override fun getAnimation(id: Int): XmlResourceParser = baseResources.getAnimation(id) 55 | 56 | override fun getDisplayMetrics(): DisplayMetrics = baseResources.displayMetrics 57 | 58 | override fun getDrawableForDensity(id: Int, density: Int): Drawable? = 59 | baseResources.getDrawableForDensity(id, density) 60 | 61 | @TargetApi(VERSION_CODES.LOLLIPOP) 62 | override fun getDrawableForDensity(id: Int, density: Int, theme: Theme?): Drawable? = 63 | baseResources.getDrawableForDensity(id, density, theme) 64 | 65 | override fun getConfiguration(): Configuration = baseResources.configuration 66 | 67 | override fun obtainAttributes(set: AttributeSet?, attrs: IntArray?): TypedArray { 68 | return baseResources.obtainAttributes(set, attrs) 69 | } 70 | 71 | @Throws(NotFoundException::class) 72 | override fun getDimensionPixelSize(id: Int): Int = baseResources.getDimensionPixelSize(id) 73 | 74 | @Throws(NotFoundException::class) 75 | override fun getIntArray(id: Int): IntArray = baseResources.getIntArray(id) 76 | 77 | @Throws(NotFoundException::class) 78 | override fun getValue(id: Int, outValue: TypedValue?, resolveRefs: Boolean) { 79 | baseResources.getValue(id, outValue, resolveRefs) 80 | } 81 | 82 | @Throws(NotFoundException::class) 83 | override fun getValue(name: String?, outValue: TypedValue?, resolveRefs: Boolean) { 84 | baseResources.getValue(name, outValue, resolveRefs) 85 | } 86 | 87 | @Throws(NotFoundException::class) 88 | override fun getResourcePackageName(resid: Int): String = 89 | baseResources.getResourcePackageName(resid) 90 | 91 | @Throws(NotFoundException::class) 92 | override fun openRawResourceFd(id: Int): AssetFileDescriptor = 93 | baseResources.openRawResourceFd(id) 94 | 95 | @Throws(NotFoundException::class) 96 | override fun getDimension(id: Int): Float = baseResources.getDimension(id) 97 | 98 | @Throws(NotFoundException::class) 99 | override fun getColorStateList(id: Int): ColorStateList = baseResources.getColorStateList(id) 100 | 101 | @TargetApi(VERSION_CODES.M) 102 | @Throws(NotFoundException::class) 103 | override fun getColorStateList(id: Int, theme: Theme?): ColorStateList = 104 | baseResources.getColorStateList(id, theme) 105 | 106 | @Throws(NotFoundException::class) 107 | override fun getBoolean(id: Int): Boolean = baseResources.getBoolean(id) 108 | 109 | override fun getIdentifier(name: String?, defType: String?, defPackage: String?): Int = 110 | baseResources.getIdentifier(name, defType, defPackage) 111 | 112 | @Throws(NotFoundException::class) 113 | override fun getColor(id: Int): Int = baseResources.getColor(id) 114 | 115 | @TargetApi(VERSION_CODES.M) 116 | @Throws(NotFoundException::class) 117 | override fun getColor(id: Int, theme: Theme?): Int = baseResources.getColor(id, theme) 118 | 119 | override fun openRawResource(id: Int): InputStream = baseResources.openRawResource(id) 120 | 121 | @Throws(NotFoundException::class) 122 | override fun openRawResource(id: Int, value: TypedValue?): InputStream = 123 | baseResources.openRawResource(id, value) 124 | 125 | @Throws(NotFoundException::class) 126 | override fun getMovie(id: Int): Movie = baseResources.getMovie(id) 127 | 128 | @Throws(NotFoundException::class) 129 | override fun getInteger(id: Int): Int = baseResources.getInteger(id) 130 | 131 | @Throws(XmlPullParserException::class, IOException::class) 132 | override fun parseBundleExtras(parser: XmlResourceParser?, outBundle: Bundle?) { 133 | this.baseResources.parseBundleExtras(parser, outBundle) 134 | } 135 | 136 | @Throws(NotFoundException::class) 137 | override fun getDrawable(id: Int): Drawable = baseResources.getDrawable(id) 138 | 139 | @TargetApi(VERSION_CODES.LOLLIPOP) 140 | @Throws(NotFoundException::class) 141 | override fun getDrawable(id: Int, theme: Theme?): Drawable = 142 | baseResources.getDrawable(id, theme) 143 | 144 | @Throws(NotFoundException::class) 145 | override fun getResourceTypeName(resid: Int): String = baseResources.getResourceTypeName(resid) 146 | 147 | @Throws(NotFoundException::class) 148 | override fun getLayout(id: Int): XmlResourceParser = baseResources.getLayout(id) 149 | 150 | @SuppressLint("NewApi") 151 | @RequiresApi(VERSION_CODES.O) 152 | @Throws(NotFoundException::class) 153 | override fun getFont(id: Int): Typeface = baseResources.getFont(id) 154 | 155 | @Throws(NotFoundException::class) 156 | override fun getXml(id: Int): XmlResourceParser = baseResources.getXml(id) 157 | 158 | @Throws(NotFoundException::class) 159 | override fun getResourceName(resid: Int): String = baseResources.getResourceName(resid) 160 | 161 | @Throws(XmlPullParserException::class) 162 | override fun parseBundleExtra(tagName: String?, attrs: AttributeSet?, outBundle: Bundle?) { 163 | baseResources.parseBundleExtra(tagName, attrs, outBundle) 164 | } 165 | 166 | @Throws(NotFoundException::class) 167 | override fun getDimensionPixelOffset(id: Int): Int = baseResources.getDimensionPixelOffset(id) 168 | 169 | @Throws(NotFoundException::class) 170 | override fun getValueForDensity( 171 | id: Int, 172 | density: Int, 173 | outValue: TypedValue?, 174 | resolveRefs: Boolean 175 | ) { 176 | baseResources.getValueForDensity(id, density, outValue, resolveRefs) 177 | } 178 | 179 | @Throws(NotFoundException::class) 180 | override fun getResourceEntryName(resid: Int): String = 181 | baseResources.getResourceEntryName(resid) 182 | 183 | @Throws(NotFoundException::class) 184 | override fun getFraction(id: Int, base: Int, pbase: Int): Float = 185 | baseResources.getFraction(id, base, pbase) 186 | } 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Philology 2 | ========= 3 | [![CircleCI](https://circleci.com/gh/JcMinarro/Philology/tree/master.svg?style=svg)](https://circleci.com/gh/JcMinarro/Philology/tree/master) [ ![Download](https://api.bintray.com/packages/jcminarro/maven/Philology/images/download.svg) ](https://bintray.com/jcminarro/maven/Philology/_latestVersion) [![ko-fi](https://www.ko-fi.com/img/donate_sm.png)](https://ko-fi.com/A50744AD) 4 | 5 | An easy way to dynamically replace Strings of your Android App or provide new languages Over-the-air without needed to publish a new release on Google Play. 6 | 7 | ## Why should I be interested in Philology 8 | 9 | ### How String resources work on Android? 10 | Android Resources provide us with an easy way to internationalise our App: a file with all the strings used by our App and a copy of it for every language the App is translated to. Android OS does the rest choosing the proper file depending on device's language. 11 | 12 | That is perfect and I don't want to stop using it. 13 | 14 | ### The problem 15 | These strings are hardcoded inside our App. If there's a typo or you find a better way to express something, a new version of the App needs to be deployed to include the newer translation. 16 | This is a slow process and a poor user experience. We all know users take their time to update an app (if they ever do so) and there's also the time Google Play takes to make a new version of an app available to all users. 17 | 18 | ### How Philology solves this problem? 19 | 20 | _Philology_ doesn't replace the way you are using resources in your current Android Development. 21 | Instead, it improves the process by intercepting the value returned from your **hardcoded translation** files inside of the app and check if there is a newer value in the server. 22 | This allows for typo fixing, better wording or even for adding a new language. All in real time, without releasing a new version of the App. 23 | 24 | With _Philology_ you could replace hardcoded texts instantly and win time before the new release is done. 25 | 26 | ## Getting Started 27 | 28 | ### Dependency 29 | Philology use [ViewPump](https://github.com/InflationX/ViewPump) library to intercept the view inflate process and reword strings resources. It allows you to use other libraries like [Calligraphy](https://github.com/InflationX/Calligraphy) that intercept the view inflate process 30 | 31 | ```groovy 32 | dependencies { 33 | compile 'com.jcminarro:Philology:2.1.0' 34 | compile 'io.github.inflationx:viewpump:2.0.3' 35 | } 36 | ``` 37 | 38 | ### Usage 39 | 40 | #### Initialize Philology and ViewPump 41 | Define your `PhilologyRepositoryFactory` who provides `PhilologyRepository` according with the selected `Locale` on the device. 42 | Kotlin: 43 | ```Kotlin 44 | class App : Application() { 45 | override fun onCreate() { 46 | super.onCreate() 47 | // Init Philology with our PhilologyRepositoryFactory 48 | Philology.init(MyPhilologyRepositoryFactory) 49 | // Add PhilologyInterceptor to ViewPump 50 | // If you are already using Calligraphy you can add both interceptors, there is no problem 51 | ViewPump.init(ViewPump.builder().addInterceptor(PhilologyInterceptor).build()) 52 | } 53 | } 54 | 55 | object MyPhilologyRepositoryFactory : PhilologyRepositoryFactory { 56 | override fun getPhilologyRepository(locale: Locale): PhilologyRepository? = when (locale.language) { 57 | Locale.ENGLISH.language -> EnglishPhilologyRepository 58 | Locale("es", "ES").language -> SpanishPhilologyRepository 59 | // If we don't support a language we could return null as PhilologyRepository and 60 | // values from the strings resources file will be used 61 | else -> null 62 | } 63 | } 64 | 65 | object EnglishPhilologyRepository : PhilologyRepository { 66 | override fun getText(key: String): CharSequence? = when (key) { 67 | "label" -> "New value for the `label` key, it could be fetched from a database or an external API server" 68 | // If we don't want reword a strings we could return null and the value from the string resources file will be used 69 | else -> null 70 | } 71 | 72 | override fun getPlural(key: String, quantityString: String): CharSequence? = when ("${key}_$quantityString") { 73 | "plurals_label_one" -> "New value for the `plurals_label` key and `one` quantity keyword" 74 | "plurals_label_other" -> "New value for the `plurals_label` key and `other` quantity keyword" 75 | // If we don't want reword a plural we could return null and the value from the string resources file will be used 76 | else -> null 77 | } 78 | 79 | override fun getTextArray(key: String): Array? = when (key) { 80 | "days" -> arrayOf( 81 | "Monday", 82 | "Tuesday", 83 | "Wednesday", 84 | "Thursday", 85 | "Friday", 86 | "Saturday", 87 | "Saturday" 88 | ) 89 | // If we don't want reword a string array we could return null and the value from the string resources file will be used 90 | else -> null 91 | } 92 | } 93 | 94 | object SpanishPhilologyRepository : PhilologyRepository { /* Implementation */ } 95 | ``` 96 | 97 | Java: 98 | ```java 99 | public class App extends Application { 100 | @Override 101 | public void onCreate() { 102 | super.onCreate(); 103 | PhilologyRepositoryFactory repositoryFactory = new MyPhilologyRepositoryFactory(); 104 | Philology.INSTANCE.init(repositoryFactory); 105 | ViewPump.init(ViewPump.builder().addInterceptor(PhilologyInterceptor.INSTANCE).build()); 106 | } 107 | } 108 | 109 | public class MyPhilologyRepositoryFactory extends PhilologyRepositoryFactory { 110 | @Nullable 111 | @Override 112 | public PhilologyRepository getPhilologyRepository(@NotNull Locale locale) { 113 | if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { 114 | return new EnglishPhilologyRepository(); 115 | } 116 | return null; 117 | } 118 | } 119 | 120 | public class EnglishPhilologyRepository extends PhilologyRepository { 121 | @Nullable 122 | @Override 123 | public CharSequence getText(@NotNull Resource resource) { /* Implementation */} 124 | } 125 | ``` 126 | 127 | #### Inject into Context 128 | Wrap the `Activity` Context. 129 | Kotlin: 130 | ```kotlin 131 | class BaseActivity : AppCompatActivity() { 132 | override fun attachBaseContext(newBase: Context) { 133 | super.attachBaseContext(ViewPumpContextWrapper.wrap(Philology.wrap(newBase))) 134 | } 135 | } 136 | ``` 137 | 138 | Java: 139 | ```java 140 | public class BaseActivity extends AppCompatActivity { 141 | @Override 142 | protected void attachBaseContext(Context newBase) { 143 | super.attachBaseContext(ViewPumpContextWrapper.wrap(Philology.INSTANCE.wrap(newBase))); 144 | } 145 | } 146 | ``` 147 | 148 | _That is all_ 149 | 150 | #### CustomViews 151 | Philology allows you to reword your own CustomViews, the only that you need to do is provide an implementation of `ViewTransformer` that rewords the text fields used by your CustomView. The `Context` used to inflate your CustomView is already wrapped by the library, so, you can assign the `@StringRes` used on the `.xml` file that will be provided on the `reword()` method. 152 | 153 | Kotlin: 154 | ```kotlin 155 | object MyCustomViewTransformer : ViewTransformer { 156 | override fun reword(view: View, attributeSet: AttributeSet): View = view.apply { 157 | when (this) { 158 | is MyCustomView -> reword(attributeSet) 159 | } 160 | } 161 | 162 | private fun MyCustomView.reword(attributeSet: AttributeSet) { 163 | @StringRes val textResId = context.getStringResourceId(attributeSet, R.styleable.MyCustomView_text) 164 | if (textResId > 0) setTextToMyCustomView(textResId) 165 | } 166 | } 167 | ``` 168 | 169 | Java: 170 | ```java 171 | public class MyCustomViewTransformer extends ViewTransformer { 172 | private static String MY_CUSTOM_ATTRIBUTE_NAME = "text"; 173 | @NotNull 174 | @Override 175 | public View reword(@NotNull View view, @NotNull AttributeSet attributeSet) { 176 | if (view instanceof MyCustomView) { 177 | MyCustomView myCustomView = (MyCustomView) view; 178 | int textResId = getStringResourceId(myCustomView.getContext(), attributeSet, R.styleable.MyCustomView_text); 179 | if (textResId > 0) { 180 | myCustomView.setTextToMyCustomView(textResId); 181 | } 182 | } 183 | return view; 184 | } 185 | } 186 | ``` 187 | 188 | After you implement your `ViewTransformer` you need to provide it to `Philology` injecting a `ViewTransformerFactory` by the `init()` method 189 | Kotlin: 190 | ```kotlin 191 | class App : Application() { 192 | override fun onCreate() { 193 | super.onCreate() 194 | Philology.init(MyPhilologyRepositoryFactory, MyViewTransformerFactory) 195 | ViewPump.init(ViewPump.builder().addInterceptor(PhilologyInterceptor.INSTANCE).build()); 196 | } 197 | } 198 | 199 | object MyViewTransformerFactory : ViewTransformerFactory{ 200 | override fun getViewTransformer(view: View): ViewTransformer = when (view) { 201 | is MyCustomView -> MyCustomViewTransformer 202 | else -> null 203 | } 204 | } 205 | ``` 206 | 207 | Java: 208 | ```java 209 | 210 | public class App extends Application { 211 | @Override 212 | public void onCreate() { 213 | super.onCreate(); 214 | PhilologyRepositoryFactory repositoryFactory = new MyPhilologyRepositoryFactory(); 215 | ViewTransformerFactory viewTransformerFactory = new MyCustomViewTransformerFactory(); 216 | Philology.INSTANCE.init(repositoryFactory, viewTransformerFactory); 217 | ViewPump.init(ViewPump.builder().addInterceptor(PhilologyInterceptor.INSTANCE).build()); 218 | } 219 | } 220 | 221 | public class MyCustomViewTransformerFactory implements ViewTransformerFactory { 222 | @Nullable 223 | @Override 224 | public ViewTransformer getViewTransformer(@NotNull View view) { 225 | if (view instanceof MyCustomView) { 226 | return new MyViewTransformer(); 227 | } 228 | return null; 229 | } 230 | } 231 | ``` 232 | 233 | ## Do you want to contribute? 234 | Feel free to add any useful feature to the library, we will be glad to improve it with your help. 235 | I'd love to hear about your use case too, especially if it's not covered perfectly. 236 | 237 | Developed By 238 | ------------ 239 | 240 | * Jc Miñarro - 241 | 242 | 243 | Follow me on Twitter 244 | 245 | 246 | Add me to Linkedin 247 | 248 | 249 | Follow me on Twitter 250 | 251 | 252 | License 253 | ------- 254 | 255 | Copyright 2018 Jc Miñarro 256 | 257 | Licensed under the Apache License, Version 2.0 (the "License"); 258 | you may not use this file except in compliance with the License. 259 | You may obtain a copy of the License at 260 | 261 | http://www.apache.org/licenses/LICENSE-2.0 262 | 263 | Unless required by applicable law or agreed to in writing, software 264 | distributed under the License is distributed on an "AS IS" BASIS, 265 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 266 | See the License for the specific language governing permissions and 267 | limitations under the License. 268 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /philology/src/test/java/com/jcminarro/philology/ResourcesUtilTest.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology 2 | 3 | import android.content.res.Configuration 4 | import android.content.res.Resources 5 | import com.nhaarman.mockito_kotlin.doReturn 6 | import org.amshove.kluent.When 7 | import org.amshove.kluent.`should be equal to` 8 | import org.amshove.kluent.`should throw` 9 | import org.amshove.kluent.calling 10 | import org.amshove.kluent.invoking 11 | import org.amshove.kluent.mock 12 | import org.junit.Before 13 | import org.junit.Test 14 | import java.util.Locale 15 | 16 | class ResourcesUtilTest { 17 | 18 | private var configuration: Configuration = createConfiguration() 19 | private var quantity = 1 20 | private val baseResources: Resources = mock() 21 | private val resources = ResourcesUtil(baseResources) 22 | private val someCharSequence: CharSequence = "text" 23 | private val someString: String = someCharSequence.toString() 24 | private val repoCharSequence: CharSequence = "repo" 25 | private val repoString: String = repoCharSequence.toString() 26 | private val id = 0 27 | private val formatArg = "argument" 28 | private val nameId = "nameId" 29 | 30 | @Before 31 | fun setup() { 32 | clearPhilology() 33 | When calling baseResources.configuration doReturn configuration 34 | } 35 | 36 | @Test 37 | fun `Should throw an exception if the given id doesn't exist asking for a text`() { 38 | configureResourceGetIdException(baseResources, id) 39 | invoking { resources.getText(id) } `should throw` Resources.NotFoundException::class 40 | } 41 | 42 | @Test 43 | fun `Should return a CharSequence asking for a text`() { 44 | configureResourceGetText(baseResources, id, nameId, someCharSequence) 45 | resources.getText(id) `should be equal to` someCharSequence 46 | } 47 | 48 | @Test 49 | fun `Should throw an exception if the given id doesn't exist asking for an String`() { 50 | configureResourceGetIdException(baseResources, id) 51 | invoking { resources.getString(id) } `should throw` Resources.NotFoundException::class 52 | } 53 | 54 | @Test 55 | fun `Should return a CharSequence asking for an String`() { 56 | configureResourceGetText(baseResources, id, nameId, someCharSequence) 57 | resources.getString(id) `should be equal to` someString 58 | } 59 | 60 | @Test 61 | fun `Should return a CharSequence from repository asking for a text`() { 62 | configureResourceGetText(baseResources, id, nameId, someCharSequence) 63 | configurePhilology(createRepository(nameId, null, repoCharSequence)) 64 | resources.getText(id) `should be equal to` repoCharSequence 65 | } 66 | 67 | @Test 68 | fun `Should return a CharSequence from repository asking for an String`() { 69 | configureResourceGetText(baseResources, id, nameId, someCharSequence) 70 | configurePhilology(createRepository(nameId, null, repoCharSequence)) 71 | resources.getString(id) `should be equal to` repoString 72 | } 73 | 74 | @Test 75 | fun `Should throw an exception if the given id doesn't exist asking for a quantity text`() { 76 | configureResourceGetIdException(baseResources, id) 77 | invoking { resources.getQuantityText(id, quantity) } `should throw` Resources.NotFoundException::class 78 | } 79 | 80 | @Test 81 | fun `Should return a CharSequence asking for a quantity text`() { 82 | configureResourceQuantityString(baseResources, quantity, "one") 83 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 84 | resources.getQuantityText(id, quantity) `should be equal to` someCharSequence 85 | } 86 | 87 | @Test 88 | fun `Should throw an exception if the given id doesn't exist asking for an quantity String`() { 89 | configureResourceGetIdException(baseResources, id) 90 | invoking { resources.getQuantityString(id, quantity) } `should throw` Resources.NotFoundException::class 91 | } 92 | 93 | @Test 94 | fun `Should return a CharSequence asking for an quantity String`() { 95 | configureResourceQuantityString(baseResources, quantity, "one") 96 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 97 | resources.getQuantityString(id, quantity) `should be equal to` someString 98 | } 99 | 100 | @Test 101 | fun `Should throw an exception if id doesn't exist asking for an formatted quantity String`() { 102 | configureResourceGetIdException(baseResources, id) 103 | invoking { resources.getQuantityString(id, quantity, formatArg) } `should throw` Resources.NotFoundException::class 104 | } 105 | 106 | @Test 107 | fun `Should return a CharSequence asking for an formatted quantity String`() { 108 | configureResourceQuantityString(baseResources, quantity, "one") 109 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, "$someCharSequence%s") 110 | resources.getQuantityString(id, quantity, formatArg) `should be equal to` someString + formatArg 111 | } 112 | 113 | @Test 114 | fun `Should return a CharSequence for zero keyword asking for a quantity text`() { 115 | quantity = 0 116 | val locale = Locale("ar", "ME") 117 | configuration = createConfiguration(locale) 118 | setup() 119 | configureResourceQuantityString(baseResources, quantity, "zero") 120 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 121 | configurePhilology(createRepository(nameId, "zero", repoCharSequence), locale) 122 | resources.getQuantityText(id, quantity) `should be equal to` repoCharSequence 123 | } 124 | 125 | @Test 126 | fun `Should return a CharSequence for zero keyword asking for an quantity String`() { 127 | quantity = 0 128 | val locale = Locale("ar", "ME") 129 | configuration = createConfiguration(locale) 130 | setup() 131 | configureResourceQuantityString(baseResources, quantity, "zero") 132 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 133 | configurePhilology(createRepository(nameId, "zero", repoCharSequence), locale) 134 | resources.getQuantityText(id, quantity) `should be equal to` repoString 135 | } 136 | 137 | @Test 138 | fun `Should return a CharSequence for one keyword asking for a quantity text`() { 139 | quantity = 1 140 | configureResourceQuantityString(baseResources, quantity, "one") 141 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 142 | configurePhilology(createRepository(nameId, "one", repoCharSequence)) 143 | resources.getQuantityText(id, quantity) `should be equal to` repoCharSequence 144 | } 145 | 146 | @Test 147 | fun `Should return a CharSequence for one keyword asking for an quantity String`() { 148 | quantity = 1 149 | configureResourceQuantityString(baseResources, quantity, "one") 150 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 151 | configurePhilology(createRepository(nameId, "one", repoCharSequence)) 152 | resources.getQuantityText(id, quantity) `should be equal to` repoString 153 | } 154 | 155 | @Test 156 | fun `Should return a CharSequence for two keyword asking for a quantity text`() { 157 | quantity = 2 158 | val locale = Locale("ar", "ME") 159 | configuration = createConfiguration(locale) 160 | setup() 161 | configureResourceQuantityString(baseResources, quantity, "two") 162 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 163 | configurePhilology(createRepository(nameId, "two", repoCharSequence), locale) 164 | resources.getQuantityText(id, quantity) `should be equal to` repoCharSequence 165 | } 166 | 167 | @Test 168 | fun `Should return a CharSequence for two keyword asking for an quantity String`() { 169 | quantity = 2 170 | val locale = Locale("ar", "ME") 171 | configuration = createConfiguration(locale) 172 | setup() 173 | configureResourceQuantityString(baseResources, quantity, "two") 174 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 175 | configurePhilology(createRepository(nameId, "two", repoCharSequence), locale) 176 | resources.getQuantityText(id, quantity) `should be equal to` repoString 177 | } 178 | 179 | @Test 180 | fun `Should return a CharSequence for few keyword asking for a quantity text`() { 181 | quantity = 2 182 | val locale = Locale("ru", "RU") 183 | configuration = createConfiguration(locale) 184 | setup() 185 | configureResourceQuantityString(baseResources, quantity, "few") 186 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 187 | configurePhilology(createRepository(nameId, "few", repoCharSequence), locale) 188 | resources.getQuantityText(id, quantity) `should be equal to` repoCharSequence 189 | } 190 | 191 | @Test 192 | fun `Should return a CharSequence for few keyword asking for an quantity String`() { 193 | quantity = 2 194 | val locale = Locale("ru", "RU") 195 | configuration = createConfiguration(locale) 196 | setup() 197 | configureResourceQuantityString(baseResources, quantity, "few") 198 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 199 | configurePhilology(createRepository(nameId, "few", repoCharSequence), locale) 200 | resources.getQuantityText(id, quantity) `should be equal to` repoString 201 | } 202 | 203 | @Test 204 | fun `Should return a CharSequence for many keyword asking for a quantity text`() { 205 | quantity = 5 206 | val locale = Locale("ru", "RU") 207 | configuration = createConfiguration(locale) 208 | setup() 209 | configureResourceQuantityString(baseResources, quantity, "many") 210 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 211 | configurePhilology(createRepository(nameId, "many", repoCharSequence), locale) 212 | resources.getQuantityText(id, quantity) `should be equal to` repoCharSequence 213 | } 214 | 215 | @Test 216 | fun `Should return a CharSequence for many keyword asking for an quantity String`() { 217 | quantity = 5 218 | val locale = Locale("ru", "RU") 219 | configuration = createConfiguration(locale) 220 | setup() 221 | configureResourceQuantityString(baseResources, quantity, "many") 222 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 223 | configurePhilology(createRepository(nameId, "many", repoCharSequence), locale) 224 | resources.getQuantityText(id, quantity) `should be equal to` repoString 225 | } 226 | 227 | @Test 228 | fun `Should return a CharSequence for other keyword asking for a quantity text`() { 229 | quantity = 2 230 | configureResourceQuantityString(baseResources, quantity, "other") 231 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 232 | configurePhilology(createRepository(nameId, "other", repoCharSequence)) 233 | resources.getQuantityText(id, quantity) `should be equal to` repoCharSequence 234 | } 235 | 236 | @Test 237 | fun `Should return a CharSequence for other keyword asking for an quantity String`() { 238 | quantity = 2 239 | configureResourceQuantityString(baseResources, quantity, "other") 240 | configureResourceGetQuantityText(baseResources, id, nameId, quantity, someCharSequence) 241 | configurePhilology(createRepository(nameId, "other", repoCharSequence)) 242 | resources.getQuantityText(id, quantity) `should be equal to` repoString 243 | } 244 | 245 | @Test 246 | fun `Should throw an exception if the given id doesn't exist asking for text array`() { 247 | configureResourceGetIdException(baseResources, id) 248 | invoking { resources.getTextArray(id) } `should throw` Resources.NotFoundException::class 249 | } 250 | 251 | @Test 252 | fun `Should return an array of strings asking for a text array`() { 253 | val textArray: Array = arrayOf("first", "second") 254 | configureResourceGetTextArray(baseResources, id, nameId, textArray) 255 | resources.getTextArray(id) `should be equal to` textArray 256 | } 257 | 258 | @Test 259 | fun `Should throw an exception if the given id doesn't exist asking for a string array`() { 260 | configureResourceGetIdException(baseResources, id) 261 | invoking { resources.getStringArray(id) } `should throw` Resources.NotFoundException::class 262 | } 263 | 264 | @Test 265 | fun `Should return an array of strings asking for a string array`() { 266 | val textArray: Array = arrayOf("first", "second") 267 | configureResourceGetTextArray(baseResources, id, nameId, textArray) 268 | resources.getStringArray(id) `should be equal to` textArray.map { it.toString() }.toTypedArray() 269 | } 270 | 271 | @Test 272 | fun `Should return an array of strings from repository asking for a text array`() { 273 | val textArray: Array = arrayOf("first", "second") 274 | configureResourceGetTextArray(baseResources, id, nameId, textArray) 275 | configurePhilology(createRepository(nameId, textArray = textArray)) 276 | resources.getTextArray(id) `should be equal to` textArray 277 | } 278 | 279 | @Test 280 | fun `Should return an array of strings from repository asking for a string array`() { 281 | val textArray: Array = arrayOf("first", "second") 282 | configureResourceGetTextArray(baseResources, id, nameId, textArray) 283 | configurePhilology(createRepository(nameId, textArray = textArray)) 284 | resources.getStringArray(id) `should be equal to` textArray.map { it.toString() }.toTypedArray() 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /philology/src/test/java/com/jcminarro/philology/PhilologyResourcesTest.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.philology 2 | 3 | import android.content.res.AssetFileDescriptor 4 | import android.content.res.ColorStateList 5 | import android.content.res.Configuration 6 | import android.content.res.Resources 7 | import android.content.res.Resources.Theme 8 | import android.content.res.TypedArray 9 | import android.content.res.XmlResourceParser 10 | import android.graphics.Movie 11 | import android.graphics.Typeface 12 | import android.graphics.drawable.Drawable 13 | import android.os.Bundle 14 | import android.util.AttributeSet 15 | import android.util.DisplayMetrics 16 | import android.util.TypedValue 17 | import com.nhaarman.mockito_kotlin.doReturn 18 | import com.nhaarman.mockito_kotlin.verify 19 | import org.amshove.kluent.When 20 | import org.amshove.kluent.`should be equal to` 21 | import org.amshove.kluent.`should throw` 22 | import org.amshove.kluent.calling 23 | import org.amshove.kluent.invoking 24 | import org.amshove.kluent.mock 25 | import org.junit.Before 26 | import org.junit.Test 27 | import java.io.InputStream 28 | 29 | class PhilologyResourcesTest { 30 | 31 | private val baseResources: Resources = mock() 32 | private val configuration: Configuration = createConfiguration() 33 | private val resources = PhilologyResources(baseResources) 34 | private val someCharSequence: CharSequence = "text" 35 | private val someString: String = someCharSequence.toString() 36 | private val repoCharSequence: CharSequence = "repo" 37 | private val repoString: String = repoCharSequence.toString() 38 | private val id = 0 39 | private val nameId = "nameId" 40 | 41 | @Before 42 | fun setup() { 43 | clearPhilology() 44 | When calling baseResources.configuration doReturn configuration 45 | } 46 | 47 | @Test 48 | fun `Should throw an exception if the given id doesn't exist asking for a text`() { 49 | configureResourceGetIdException(baseResources, id) 50 | invoking { resources.getText(id) } `should throw` Resources.NotFoundException::class 51 | } 52 | 53 | @Test 54 | fun `Should return a CharSequence asking for a text`() { 55 | configureResourceGetText(baseResources, id, nameId, someCharSequence) 56 | resources.getText(id) `should be equal to` someCharSequence 57 | } 58 | 59 | @Test 60 | fun `Should return a defaultCharSequence asking for a text with a default value`() { 61 | val defaultCharSequence: CharSequence = "default char sequence" 62 | configureResourceGetIdException(baseResources, id) 63 | resources.getText(id, defaultCharSequence) `should be equal to` defaultCharSequence 64 | } 65 | 66 | @Test 67 | fun `Should return a CharSequence asking for a text with a default value`() { 68 | val defaultCharSequence: CharSequence = "default char sequence" 69 | configureResourceGetText(baseResources, id, nameId, someCharSequence) 70 | resources.getText(id, defaultCharSequence) `should be equal to` someCharSequence 71 | } 72 | 73 | @Test 74 | fun `Should throw an exception if the given id doesn't exist asking for an String`() { 75 | configureResourceGetIdException(baseResources, id) 76 | invoking { resources.getString(id) } `should throw` Resources.NotFoundException::class 77 | } 78 | 79 | @Test 80 | fun `Should return a CharSequence asking for an String`() { 81 | configureResourceGetText(baseResources, id, nameId, someCharSequence) 82 | resources.getString(id) `should be equal to` someString 83 | } 84 | 85 | @Test 86 | fun `Should throw an exception if the given id doesn't exist asking for a quantity text`() { 87 | configureResourceGetIdException(baseResources, id) 88 | invoking { resources.getQuantityText(id, 1) } `should throw` Resources.NotFoundException::class 89 | } 90 | 91 | @Test 92 | fun `Should return a CharSequence asking for a quantity text`() { 93 | configureResourceQuantityString(baseResources, 1, "one") 94 | configureResourceGetQuantityText(baseResources, id, nameId, 1, someCharSequence) 95 | resources.getQuantityText(id, 1) `should be equal to` someCharSequence 96 | } 97 | 98 | @Test 99 | fun `Should throw an exception if the given id doesn't exist asking for an quantity string`() { 100 | configureResourceGetIdException(baseResources, id) 101 | invoking { resources.getQuantityString(id, 1) } `should throw` Resources.NotFoundException::class 102 | } 103 | 104 | @Test 105 | fun `Should return a CharSequence asking for an quantity string`() { 106 | configureResourceQuantityString(baseResources, 1, "one") 107 | configureResourceGetQuantityText(baseResources, id, nameId, 1, someCharSequence) 108 | resources.getQuantityString(id, 1) `should be equal to` someString 109 | } 110 | 111 | @Test 112 | fun `Should throw an exception if the given id doesn't exist asking for a string array`() { 113 | configureResourceGetIdException(baseResources, id) 114 | invoking { resources.getStringArray(id) } `should throw` Resources.NotFoundException::class 115 | } 116 | 117 | @Test 118 | fun `Should return a CharSequence asking for a string array`() { 119 | val textArray = arrayOf("first", "second") 120 | configureResourceGetTextArray(baseResources, id, nameId, textArray) 121 | resources.getStringArray(id) `should be equal to` textArray.map { it.toString() }.toTypedArray() 122 | } 123 | 124 | @Test 125 | fun `Should throw an exception if the given id doesn't exist asking for a text array`() { 126 | configureResourceGetIdException(baseResources, id) 127 | invoking { resources.getStringArray(id) } `should throw` Resources.NotFoundException::class 128 | } 129 | 130 | @Test 131 | fun `Should return a CharSequence asking for a text array`() { 132 | val textArray = arrayOf("first", "second") 133 | configureResourceGetTextArray(baseResources, id, nameId, textArray) 134 | resources.getTextArray(id) `should be equal to` textArray 135 | } 136 | 137 | @Test 138 | fun `Should return a CharSequence from repository asking for a text`() { 139 | configureResourceGetText(baseResources, id, nameId, someCharSequence) 140 | configurePhilology(createRepository(nameId, null, repoCharSequence)) 141 | resources.getText(id) `should be equal to` repoCharSequence 142 | } 143 | 144 | @Test 145 | fun `Should return a CharSequence from repository asking for an String`() { 146 | configureResourceGetText(baseResources, id, nameId, someCharSequence) 147 | configurePhilology(createRepository(nameId, null, repoCharSequence)) 148 | resources.getString(id) `should be equal to` repoString 149 | } 150 | 151 | @Test 152 | fun `Should return a CharSequence from repository asking for a quantity text`() { 153 | configureResourceQuantityString(baseResources, 1, "one") 154 | configureResourceGetQuantityText(baseResources, id, nameId, 1, someCharSequence) 155 | configurePhilology(createRepository(nameId, "one", repoCharSequence)) 156 | resources.getQuantityText(id, 1) `should be equal to` repoCharSequence 157 | } 158 | 159 | @Test 160 | fun `Should return a CharSequence from repository asking for an quantity string`() { 161 | configureResourceQuantityString(baseResources, 1, "one") 162 | configureResourceGetQuantityText(baseResources, id, nameId, 1, someCharSequence) 163 | configurePhilology(createRepository(nameId, "one", repoCharSequence)) 164 | resources.getQuantityString(id, 1) `should be equal to` repoString 165 | } 166 | 167 | @Test 168 | fun `Should return a CharSequence from repository asking for a string array`() { 169 | val textArray: Array = arrayOf("first", "second") 170 | configureResourceGetTextArray(baseResources, id, nameId, textArray) 171 | configurePhilology(createRepository(nameId, textArray = textArray)) 172 | resources.getStringArray(id) `should be equal to` textArray.map { it.toString() }.toTypedArray() 173 | } 174 | 175 | @Test 176 | fun `Should return a CharSequence from repository asking for a text array`() { 177 | val textArray: Array = arrayOf("first", "second") 178 | configureResourceGetTextArray(baseResources, id, nameId, textArray) 179 | configurePhilology(createRepository(nameId, textArray = textArray)) 180 | resources.getTextArray(id) `should be equal to` textArray 181 | } 182 | 183 | @Test 184 | fun `Should return an XmlResourceParser from base resources asking for an Animation`() { 185 | val xmlResourceParser = mock() 186 | When calling baseResources.getAnimation(id) doReturn xmlResourceParser 187 | resources.getAnimation(id) `should be equal to` xmlResourceParser 188 | } 189 | 190 | @Test 191 | fun `Should return a DisplayMetrics from base resources asking for a display metrics`() { 192 | val displayMetrics = mock() 193 | When calling baseResources.displayMetrics doReturn displayMetrics 194 | resources.displayMetrics `should be equal to` displayMetrics 195 | } 196 | 197 | @Test 198 | fun `Should return a Drawable from base resources asking for a drawable for density`() { 199 | val drawable = mock() 200 | When calling baseResources.getDrawableForDensity(id, 2) doReturn drawable 201 | resources.getDrawableForDensity(id, 2) `should be equal to` drawable 202 | } 203 | 204 | @Test 205 | fun `Should return a Drawable from base resources asking for a drawable for density on API 21+`() { 206 | val drawable = mock() 207 | val theme = mock() 208 | When calling baseResources.getDrawableForDensity(id, 2, theme) doReturn drawable 209 | resources.getDrawableForDensity(id, 2, theme) `should be equal to` drawable 210 | } 211 | 212 | @Test 213 | fun `Should return Configuration from base resources asking for a configuration`() { 214 | val configuration = mock() 215 | When calling baseResources.configuration doReturn configuration 216 | resources.configuration `should be equal to` configuration 217 | } 218 | 219 | @Test 220 | fun `Should return TypedArray from base resources asking for a attributes`() { 221 | val attributes = mock() 222 | val attributeSet = mock() 223 | val attrs = IntArray(3) 224 | When calling baseResources.obtainAttributes(attributeSet, attrs) doReturn attributes 225 | resources.obtainAttributes(attributeSet, attrs) `should be equal to` attributes 226 | } 227 | 228 | @Test 229 | fun `Should return Int from base resources asking for a dimension pixel size`() { 230 | When calling baseResources.getDimensionPixelSize(id) doReturn 15 231 | resources.getDimensionPixelSize(id) `should be equal to` 15 232 | } 233 | 234 | @Test 235 | fun `Should return IntArray from base resources asking for a int array`() { 236 | val intArray = IntArray(5) 237 | When calling baseResources.getIntArray(id) doReturn intArray 238 | resources.getIntArray(id) `should be equal to` intArray 239 | } 240 | 241 | @Test 242 | fun `Should call getValue method from base resources asking value by name`() { 243 | val outValue = mock() 244 | resources.getValue(id, outValue, true) 245 | verify(baseResources).getValue(id, outValue, true) 246 | } 247 | 248 | @Test 249 | fun `Should call getValue method from base resources asking value by id`() { 250 | val outValue = mock() 251 | resources.getValue("id", outValue, true) 252 | verify(baseResources).getValue("id", outValue, true) 253 | } 254 | 255 | @Test 256 | fun `Should return String from base resources asking for a resource package name`() { 257 | val packageName = "com.package.name" 258 | When calling baseResources.getResourcePackageName(id) doReturn packageName 259 | resources.getResourcePackageName(id) `should be equal to` packageName 260 | } 261 | 262 | @Test 263 | fun `Should return AssetFileDescriptor from base resources on open raw resource`() { 264 | val fileDescriptor = mock() 265 | When calling baseResources.openRawResourceFd(id) doReturn fileDescriptor 266 | resources.openRawResourceFd(id) `should be equal to` fileDescriptor 267 | } 268 | 269 | @Test 270 | fun `Should return Float from base resources asking for a dimension`() { 271 | val dimension = 15f 272 | When calling baseResources.getDimension(id) doReturn dimension 273 | resources.getDimension(id) `should be equal to` dimension 274 | } 275 | 276 | @Test 277 | fun `Should return ColorStateList from base resources asking for a color state list`() { 278 | val colorStateList = mock() 279 | When calling baseResources.getColorStateList(id) doReturn colorStateList 280 | resources.getColorStateList(id) `should be equal to` colorStateList 281 | } 282 | 283 | @Test 284 | fun `Should return ColorStateList from base resources asking for a color state list on API 23+`() { 285 | val colorStateList = mock() 286 | val theme = mock() 287 | When calling baseResources.getColorStateList(id, theme) doReturn colorStateList 288 | resources.getColorStateList(id, theme) `should be equal to` colorStateList 289 | } 290 | 291 | @Test 292 | fun `Should return Boolean from base resources asking for a boolean`() { 293 | When calling baseResources.getBoolean(id) doReturn true 294 | resources.getBoolean(id) `should be equal to` true 295 | } 296 | 297 | @Test 298 | fun `Should return Int from base resources asking for a identifier`() { 299 | When calling baseResources.getIdentifier("name", "defType", "defPackage") doReturn 5 300 | resources.getIdentifier("name", "defType", "defPackage") `should be equal to` 5 301 | } 302 | 303 | @Test 304 | fun `Should return Int from base resources asking for a color`() { 305 | When calling baseResources.getColor(id) doReturn 15 306 | resources.getColor(id) `should be equal to` 15 307 | } 308 | 309 | @Test 310 | fun `Should return Int from base resources asking for a color on API 23+`() { 311 | val theme = mock() 312 | When calling baseResources.getColor(id, theme) doReturn 15 313 | resources.getColor(id, theme) `should be equal to` 15 314 | } 315 | 316 | @Test 317 | fun `Should return InputStream from base resources on open raw resource`() { 318 | val inputStream = mock() 319 | When calling baseResources.openRawResource(id) doReturn inputStream 320 | resources.openRawResource(id) `should be equal to` inputStream 321 | } 322 | 323 | @Test 324 | fun `Should return InputStream from base resources on open raw resource with typed value param`() { 325 | val inputStream = mock() 326 | val typedValue = mock() 327 | When calling baseResources.openRawResource(id, typedValue) doReturn inputStream 328 | resources.openRawResource(id, typedValue) `should be equal to` inputStream 329 | } 330 | 331 | @Test 332 | fun `Should return Movie from base resources asking for a movie`() { 333 | val movie = mock() 334 | When calling baseResources.getMovie(id) doReturn movie 335 | resources.getMovie(id) `should be equal to` movie 336 | } 337 | 338 | @Test 339 | fun `Should return Integer from base resources asking for a integer`() { 340 | When calling baseResources.getInteger(id) doReturn 7 341 | resources.getInteger(id) `should be equal to` 7 342 | } 343 | 344 | @Test 345 | fun `Should call parseBundleExtras method from base resources on parse bundle extras`() { 346 | val parser = mock() 347 | val bundle = Bundle() 348 | resources.parseBundleExtras(parser, bundle) 349 | verify(baseResources).parseBundleExtras(parser, bundle) 350 | } 351 | 352 | @Test 353 | fun `Should return Drawable from base resources asking for a drawable`() { 354 | val drawable = mock() 355 | When calling baseResources.getDrawable(id) doReturn drawable 356 | resources.getDrawable(id) `should be equal to` drawable 357 | } 358 | 359 | @Test 360 | fun `Should return Drawable from base resources asking for a drawable on API 21+`() { 361 | val drawable = mock() 362 | val theme = mock() 363 | When calling baseResources.getDrawable(id, theme) doReturn drawable 364 | resources.getDrawable(id, theme) `should be equal to` drawable 365 | } 366 | 367 | @Test 368 | fun `Should return String from base resources asking for a resource type name`() { 369 | val typeName = "typeName" 370 | When calling baseResources.getResourceTypeName(id) doReturn typeName 371 | resources.getResourceTypeName(id) `should be equal to` typeName 372 | } 373 | 374 | @Test 375 | fun `Should return XmlResourceParser from base resources asking for a layout`() { 376 | val xmlParser = mock() 377 | When calling baseResources.getLayout(id) doReturn xmlParser 378 | resources.getLayout(id) `should be equal to` xmlParser 379 | } 380 | 381 | @Test 382 | fun `Should return Typeface from base resources asking for a font`() { 383 | val typeface = mock() 384 | When calling baseResources.getFont(id) doReturn typeface 385 | resources.getFont(id) `should be equal to` typeface 386 | } 387 | 388 | @Test 389 | fun `Should return XmlResourceParser from base resources asking for a xml`() { 390 | val xmlParser = mock() 391 | When calling baseResources.getXml(id) doReturn xmlParser 392 | resources.getXml(id) `should be equal to` xmlParser 393 | } 394 | 395 | @Test 396 | fun `Should return String from base resources asking for a resource name`() { 397 | val resourceName = "resourceName" 398 | When calling baseResources.getResourceName(id) doReturn resourceName 399 | resources.getResourceName(id) `should be equal to` resourceName 400 | } 401 | 402 | @Test 403 | fun `Should call parseBundleExtra method from base resources on parse bundle extra`() { 404 | val tagName = "tagName" 405 | val attrs = mock() 406 | val bundle = Bundle() 407 | resources.parseBundleExtra(tagName, attrs, bundle) 408 | verify(baseResources).parseBundleExtra(tagName, attrs, bundle) 409 | } 410 | 411 | @Test 412 | fun `Should return Int from base resources asking for a dimension pixel offset`() { 413 | When calling baseResources.getDimensionPixelOffset(id) doReturn 15 414 | resources.getDimensionPixelOffset(id) `should be equal to` 15 415 | } 416 | 417 | @Test 418 | fun `Should call getValueForDensity method from base resources on get value for density`() { 419 | val outValue = mock() 420 | resources.getValueForDensity(id, 2, outValue, true) 421 | verify(baseResources).getValueForDensity(id, 2, outValue, true) 422 | } 423 | 424 | @Test 425 | fun `Should return String from base resources asking for a resource entry name`() { 426 | val entryName = "entryName" 427 | When calling baseResources.getResourceEntryName(id) doReturn entryName 428 | resources.getResourceEntryName(id) `should be equal to` entryName 429 | } 430 | 431 | @Test 432 | fun `Should return Float from base resources asking for a fraction`() { 433 | val fraction = 132f 434 | When calling baseResources.getFraction(id, 2, 5) doReturn fraction 435 | resources.getFraction(id, 2, 5) `should be equal to` fraction 436 | } 437 | } 438 | --------------------------------------------------------------------------------