fix: project folder naming

This commit is contained in:
2023-12-28 20:26:30 +08:00
parent 2173620443
commit c90ac4e152
18 changed files with 1 additions and 1 deletions

2
flexilocale-gradle-plugin/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.gradle
/build

View File

@@ -0,0 +1,66 @@
plugins {
`kotlin-dsl`
autowire(libs.plugins.kotlin.jvm)
autowire(libs.plugins.maven.publish)
}
allprojects {
group = property.project.groupName
version = property.project.version
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
withSourcesJar()
}
kotlin {
jvmToolchain(17)
sourceSets.all { languageSettings { languageVersion = "2.0" } }
}
dependencies {
compileOnly(com.android.library.com.android.library.gradle.plugin)
compileOnly(org.jetbrains.kotlin.kotlin.gradle.plugin)
implementation(com.squareup.kotlinpoet)
}
gradlePlugin {
plugins {
create(property.project.moduleName) {
id = property.project.groupName
implementationClass = property.gradle.plugin.implementationClass
}
}
}
mavenPublishing {
coordinates(property.project.groupName, property.project.moduleName, property.project.version)
pom {
name = property.project.name
description = property.project.description
url = property.project.url
licenses {
license {
name = property.project.licence.name
url = property.project.licence.url
distribution = property.project.licence.url
}
}
developers {
developer {
id = property.project.developer.id
name = property.project.developer.name
email = property.project.developer.email
}
}
scm {
url = property.maven.publish.scm.url
connection = property.maven.publish.scm.connection
developerConnection = property.maven.publish.scm.developerConnection
}
}
publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.S01)
signAllPublications()
}

View File

@@ -0,0 +1,41 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/10.
*/
@file:Suppress("unused")
package com.highcapable.flexilocale
import com.highcapable.flexilocale.generated.FlexiLocaleProperties
/**
* [FlexiLocale] 的装载调用类
*/
object FlexiLocale {
/** 标签名称 */
const val TAG = FlexiLocaleProperties.PROJECT_NAME
/** 版本 */
const val VERSION = FlexiLocaleProperties.PROJECT_VERSION
/** 项目地址 */
const val PROJECT_URL = FlexiLocaleProperties.PROJECT_URL
}

View File

@@ -0,0 +1,116 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/11.
*/
@file:Suppress("unused", "USELESS_CAST", "KotlinRedundantDiagnosticSuppress")
package com.highcapable.flexilocale.gradle.factory
import com.highcapable.flexilocale.utils.debug.FError
import com.highcapable.flexilocale.utils.factory.camelcase
import org.gradle.api.Action
import org.gradle.api.plugins.ExtensionAware
/**
* 创建、获取扩展方法
* @param name 方法名称 - 自动调用 [toSafeExtName]
* @param clazz 目标对象 [Class]
* @param args 方法参数
* @return [ExtensionAware]
*/
internal fun ExtensionAware.getOrCreate(name: String, clazz: Class<*>, vararg args: Any?) = name.toSafeExtName().let { sName ->
runCatching { extensions.create(sName, clazz, *args).asExtension() }.getOrElse {
if (!(it is IllegalArgumentException && it.message?.startsWith("Cannot add extension with name") == true)) throw it
runCatching { extensions.getByName(sName).asExtension() }.getOrNull() ?: FError.make("Create or get extension failed with name \"$sName\"")
}
}
/**
* 创建、获取扩展方法 - 目标对象 [T]
* @param name 方法名称 - 自动调用 [toSafeExtName]
* @param args 方法参数
* @return [T]
*/
internal inline fun <reified T> ExtensionAware.getOrCreate(name: String, vararg args: Any?) = name.toSafeExtName().let { sName ->
runCatching { extensions.create(sName, T::class.java, *args) as T }.getOrElse {
if (!(it is IllegalArgumentException && it.message?.startsWith("Cannot add extension with name") == true)) throw it
runCatching { extensions.getByName(sName) as? T? }.getOrNull() ?: FError.make("Create or get extension failed with name \"$sName\"")
}
}
/**
* 获取扩展方法
* @param name 方法名称
* @return [ExtensionAware]
*/
internal fun ExtensionAware.get(name: String) =
runCatching { extensions.getByName(name).asExtension() }.getOrNull() ?: FError.make("Could not get extension with name \"$name\"")
/**
* 获取扩展方法 - 目标对象 [T]
* @param name 方法名称
* @return [T]
*/
internal inline fun <reified T> ExtensionAware.get(name: String) =
runCatching { extensions.getByName(name) as T }.getOrNull() ?: FError.make("Could not get extension with name \"$name\"")
/**
* 获取扩展方法 - 目标对象 [T]
* @return [T]
*/
internal inline fun <reified T> ExtensionAware.get() =
runCatching { extensions.getByType(T::class.java) as T }.getOrNull() ?: FError.make("Could not get extension with type ${T::class.java}")
/**
* 配置扩展方法 - 目标对象 [T]
* @param name 方法名称
* @param configure 配置方法体
*/
internal inline fun <reified T> ExtensionAware.configure(name: String, configure: Action<T>) = extensions.configure(name, configure)
/**
* 是否存在扩展方法
* @param name 方法名称
* @return [Boolean]
*/
internal fun ExtensionAware.hasExtension(name: String) = runCatching { extensions.getByName(name); true }.getOrNull() ?: false
/**
* 转换到扩展方法类型 [ExtensionAware]
* @return [ExtensionAware]
* @throws IllegalStateException 如果类型不是 [ExtensionAware]
*/
internal fun Any.asExtension() = this as? ExtensionAware? ?: FError.make("This instance \"$this\" is not a valid Extension")
/**
* 由于 Gradle 存在一个 [ExtensionAware] 的扩展
*
* 此功能用于检测当前字符串是否为 Gradle 使用的关键字名称
* @return [Boolean]
*/
internal fun String.isUnSafeExtName() = camelcase().let { it == "ext" || it == "extra" || it == "extraProperties" || it == "extensions" }
/**
* 由于 Gradle 存在一个 [ExtensionAware] 的扩展
*
* 此功能用于转换不符合规定的字符串到 "{字符串}s"
* @return [String]
*/
internal fun String.toSafeExtName() = if (isUnSafeExtName()) "${this}s" else this

View File

@@ -0,0 +1,42 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/11.
*/
package com.highcapable.flexilocale.gradle.factory
import org.gradle.api.Project
/**
* 获取指定项目的完整名称 (无子项目前冒号)
* @return [String]
*/
internal fun Project.fullName(): String {
val baseNames = mutableListOf<String>()
/**
* 递归子项目
* @param project 当前项目
*/
fun fetchChild(project: Project) {
project.parent?.also { if (it != it.rootProject) fetchChild(it) }
baseNames.add(project.name)
}; fetchChild(project = this)
return buildString { baseNames.onEach { append(":$it") }.clear() }.drop(1)
}

View File

@@ -0,0 +1,42 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/10.
*/
package com.highcapable.flexilocale.gradle.proxy
import org.gradle.api.Project
/**
* Gradle [Project] 生命周期接口
*/
internal interface IProjectLifecycle {
/**
* 当 Gradle 开始装载项目时回调
* @param project 当前项目
*/
fun onLoaded(project: Project)
/**
* 当 Gradle 项目装载完成时回调
* @param project 当前项目
*/
fun onEvaluate(project: Project)
}

View File

@@ -0,0 +1,57 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/10.
*/
package com.highcapable.flexilocale.plugin
import com.highcapable.flexilocale.FlexiLocale
import com.highcapable.flexilocale.gradle.factory.get
import com.highcapable.flexilocale.gradle.factory.getOrCreate
import com.highcapable.flexilocale.gradle.proxy.IProjectLifecycle
import com.highcapable.flexilocale.plugin.extension.dsl.configure.FlexiLocaleConfigureExtension
import com.highcapable.flexilocale.plugin.helper.LocaleAnalysisHelper
import com.highcapable.flexilocale.utils.debug.FError
import org.gradle.api.Project
/**
* [FlexiLocale] 插件扩展类
*/
internal class FlexiLocaleExtension internal constructor() : IProjectLifecycle {
private companion object {
/** Android Gradle plugin 扩展名称 */
private const val ANDROID_EXTENSION_NAME = "android"
}
/** 当前配置方法体实例 */
private var configure: FlexiLocaleConfigureExtension? = null
override fun onLoaded(project: Project) {
runCatching {
configure = project.get(ANDROID_EXTENSION_NAME).getOrCreate<FlexiLocaleConfigureExtension>(FlexiLocaleConfigureExtension.NAME)
}.onFailure { FError.make("Configure $project got an error, ${FlexiLocale.TAG} can only supports Android projects\nCaused by: $it") }
}
override fun onEvaluate(project: Project) {
val configs = configure?.build(project) ?: FError.make("Extension \"${FlexiLocaleConfigureExtension.NAME}\" create failed")
LocaleAnalysisHelper.start(project, configs)
}
}

View File

@@ -0,0 +1,47 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/10.
*/
@file:Suppress("unused")
package com.highcapable.flexilocale.plugin
import com.highcapable.flexilocale.FlexiLocale
import com.highcapable.flexilocale.utils.debug.FError
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionAware
/**
* [FlexiLocale] 插件定义类
*/
class FlexiLocalePlugin<T : ExtensionAware> internal constructor() : Plugin<T> {
/** 当前扩展实例 */
private val extension = FlexiLocaleExtension()
override fun apply(target: T) = when (target) {
is Project -> {
extension.onLoaded(target)
target.afterEvaluate { extension.onEvaluate(project = this) }
}
else -> FError.make("${FlexiLocale.TAG} can only applied in build.gradle or build.gradle.kts, but current is $target")
}
}

View File

@@ -0,0 +1,62 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/11.
*/
package com.highcapable.flexilocale.plugin.config.proxy
import com.highcapable.flexilocale.FlexiLocale
import com.highcapable.flexilocale.generated.FlexiLocaleProperties
/**
* [FlexiLocale] 配置类接口类
*/
internal interface IFlexiLocaleConfigs {
companion object {
/**
* 默认的生成目录路径
*
* "build/generated/[FlexiLocaleProperties.PROJECT_MODULE_NAME]"
*/
internal const val DEFAULT_GENERATE_DIR_PATH = "build/generated/${FlexiLocaleProperties.PROJECT_MODULE_NAME}"
}
/** 是否启用插件 */
val isEnable: Boolean
/** 自定义生成的目录路径 */
val generateDirPath: String
/** 自定义生成的包名 */
val packageName: String
/** 自定义生成的类名 */
val className: String
/** 是否启用受限访问功能 */
val isEnableRestrictedAccess: Boolean
/**
* 获取内部 [hashCode]
* @return [Int]
*/
fun innerHashCode() = "$isEnable$generateDirPath$packageName$className$isEnableRestrictedAccess".hashCode()
}

View File

@@ -0,0 +1,118 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/11.
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package com.highcapable.flexilocale.plugin.extension.dsl.configure
import com.highcapable.flexilocale.FlexiLocale
import com.highcapable.flexilocale.gradle.factory.fullName
import com.highcapable.flexilocale.plugin.config.proxy.IFlexiLocaleConfigs
import com.highcapable.flexilocale.utils.debug.FError
import com.highcapable.flexilocale.utils.factory.uppercamelcase
import org.gradle.api.Project
/**
* [FlexiLocale] 配置方法体实现类
*/
open class FlexiLocaleConfigureExtension internal constructor() {
internal companion object {
/** [FlexiLocaleConfigureExtension] 扩展名称 */
internal const val NAME = "flexiLocale"
}
/**
* 是否启用插件
*
* 默认启用 - 如果你想关闭插件 - 在这里设置就可以了
*/
var isEnable = true
@JvmName("enable") set
/**
* 自定义生成的目录路径
*
* 你可以填写相对于当前项目的路径
*
* 默认为 [IFlexiLocaleConfigs.DEFAULT_GENERATE_DIR_PATH]
*/
var generateDirPath = IFlexiLocaleConfigs.DEFAULT_GENERATE_DIR_PATH
@JvmName("generateDirPath") set
/**
* 自定义生成的包名
*
* Android 项目默认使用 "android" 配置方法块中的 "namespace"
*/
var packageName = ""
@JvmName("packageName") set
/**
* 自定义生成的类名
*
* 默认使用当前项目的名称 + "Locale"
*/
var className = ""
@JvmName("className") set
/**
* 是否启用受限访问功能
*
* 默认不启用 - 启用后将为生成的类和方法添加 "internal" 修饰符
*/
var isEnableRestrictedAccess = false
@JvmName("enableRestrictedAccess") set
/**
* 构造 [IFlexiLocaleConfigs]
* @param project 当前项目
* @return [IFlexiLocaleConfigs]
*/
internal fun build(project: Project): IFlexiLocaleConfigs {
/** 检查合法包名 */
fun String.checkingValidPackageName() {
if (isNotBlank() && !matches("^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*$".toRegex()))
FError.make("Invalid package name \"$this\"")
}
/** 检查合法类名 */
fun String.checkingValidClassName() {
if (isNotBlank() && !matches("^[a-zA-Z][a-zA-Z0-9_]*$".toRegex()))
FError.make("Invalid class name \"$this\"")
}
packageName.checkingValidPackageName()
className.checkingValidClassName()
val currentEnable = isEnable
val currentGenerateDirPath = project.file(generateDirPath).absolutePath
val currentPackageName = packageName
val currentClassName = "${className.ifBlank { project.fullName().uppercamelcase() }}Locale"
val currentEnableRestrictedAccess = isEnableRestrictedAccess
return object : IFlexiLocaleConfigs {
override val isEnable get() = currentEnable
override val generateDirPath get() = currentGenerateDirPath
override val packageName get() = currentPackageName
override val className get() = currentClassName
override val isEnableRestrictedAccess get() = currentEnableRestrictedAccess
}
}
}

View File

@@ -0,0 +1,242 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/10.
*/
package com.highcapable.flexilocale.plugin.generator
import com.highcapable.flexilocale.FlexiLocale
import com.highcapable.flexilocale.plugin.config.proxy.IFlexiLocaleConfigs
import com.highcapable.flexilocale.plugin.generator.factory.LocaleStringMap
import com.highcapable.flexilocale.utils.debug.FError
import com.highcapable.flexilocale.utils.factory.camelcase
import com.highcapable.flexilocale.utils.factory.uppercamelcase
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.LambdaTypeName
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asTypeName
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.abs
/**
* I18ns 生成工具类
*/
internal class LocaleSourcesGenerator {
/**
* 生成 [FileSpec]
* @param configs 当前配置
* @param keyValues 键值数组
* @param namespace 命名空间
* @param packageName 包名
* @return [FileSpec]
* @throws IllegalStateException 如果生成失败
*/
internal fun build(
configs: IFlexiLocaleConfigs,
keyValues: LocaleStringMap,
namespace: String,
packageName: String
) = runCatching {
FileSpec.builder(packageName, configs.className).apply {
val selfClass = ClassName(packageName, configs.className)
val contextClass = ClassName("android.content", "Context")
val resourcesClass = ClassName("android.content.res", "Resources")
val resourcesInitializer = LambdaTypeName.get(returnType = resourcesClass, parameters = emptyList())
addAnnotation(AnnotationSpec.builder(Suppress::class).addMember("\"StringFormatInvalid\"").build())
addImport(namespace, "R")
addType(TypeSpec.classBuilder(selfClass).apply {
addKdoc(
"""
This class is generated by ${FlexiLocale.TAG} at ${SimpleDateFormat.getDateTimeInstance().format(Date())}
The content here is automatically generated according to the res/values of your projects
You can visit [here](${FlexiLocale.PROJECT_URL}) for more help
""".trimIndent()
)
if (configs.isEnableRestrictedAccess) addModifiers(KModifier.INTERNAL)
addFunction(FunSpec.constructorBuilder().addModifiers(KModifier.PRIVATE).build())
addProperty(PropertySpec.builder("context", contextClass.copy(nullable = true)).apply {
addKdoc("The current [Context] for this app or library")
addModifiers(KModifier.PRIVATE)
mutable()
initializer("null")
}.build())
addProperty(PropertySpec.builder("resources", resourcesClass.copy(nullable = true)).apply {
addKdoc("The current [Resources] for this app or library")
addModifiers(KModifier.PRIVATE)
mutable()
initializer("null")
}.build())
addProperty(PropertySpec.builder("resourcesInitializer", resourcesInitializer.copy(nullable = true)).apply {
addKdoc("The current [Resources] initializer for this app or library")
addModifiers(KModifier.PRIVATE)
mutable()
initializer("null")
}.build())
addType(TypeSpec.companionObjectBuilder().apply {
if (configs.isEnableRestrictedAccess) addModifiers(KModifier.INTERNAL)
addFunction(FunSpec.builder("attach").apply {
addKdoc(
"""
Attach [${selfClass.simpleName}] to [Context]
@param context like [android.app.Application] or [android.app.Activity]
@return [${selfClass.simpleName}]
""".trimIndent()
)
addAnnotation(JvmStatic::class)
if (configs.isEnableRestrictedAccess) addModifiers(KModifier.INTERNAL)
addParameter("context", contextClass)
addStatement("return ${selfClass.simpleName}().apply { this.context = context }")
returns(selfClass)
}.build())
addFunction(FunSpec.builder("attach").apply {
addKdoc(
"""
Attach [${selfClass.simpleName}] to [Resources]
- Note: this method will have no effect if [context] already exists
@param resources A [Resources] that exists and has not been recycled
@return [${selfClass.simpleName}]
""".trimIndent()
)
addAnnotation(JvmStatic::class)
if (configs.isEnableRestrictedAccess) addModifiers(KModifier.INTERNAL)
addParameter("resources", resourcesClass)
addStatement("return ${selfClass.simpleName}().apply { this.resources = resources }")
returns(selfClass)
}.build())
addFunction(FunSpec.builder("attach").apply {
addKdoc(
"""
Attach [${selfClass.simpleName}] to [Resources] initializer
- Note: this method will have no effect if [context] already exists
@param resourcesInitializer A [Resources] initializer returns a non-recycled instance
@return [${selfClass.simpleName}]
""".trimIndent()
)
addAnnotation(JvmStatic::class)
if (configs.isEnableRestrictedAccess) addModifiers(KModifier.INTERNAL)
addParameter("resourcesInitializer", resourcesInitializer)
addStatement("return ${selfClass.simpleName}().apply { this.resourcesInitializer = resourcesInitializer }")
returns(selfClass)
}.build())
}.build())
addProperty(PropertySpec.builder("currentResources", resourcesClass).apply {
addKdoc("The current used [Resources] for this app or library")
addModifiers(KModifier.PRIVATE)
getter(FunSpec.getterBuilder().apply {
addStatement("return context?.resources ?: resourcesInitializer?.invoke() ?: resources" +
"?: error(\"${("Unable to get Resource instance, the app may have been killed " +
"or initialization process failed").toKotlinPoetSpace()}\")")
}.build())
}.build())
keyValues.forEach { (key, contentValues) ->
val fixedKey = key.camelcase()
val getterKey = "get${key.uppercamelcase()}"
val statement = "return currentResources.getString(R.string.$key, *formatArgs)"
var kDoc = "Resolve the [R.string.$key]\n\n"
if (contentValues.isNotEmpty()) kDoc += "| Configuration | Value |\n| --- | --- |\n"
contentValues.toList()
.sortedWith(compareBy<Pair<String, String>> { it.first != "default" }.thenBy { it.first })
.toAutoWrapKeyValues()
.forEach { (key, value) ->
val displayValue = value.replace("%".toRegex(), "%%")
kDoc += "| $key | $displayValue |\n"
}; kDoc = kDoc.trim()
addProperty(PropertySpec.builder(fixedKey, String::class).apply {
addKdoc(kDoc)
if (configs.isEnableRestrictedAccess) addModifiers(KModifier.INTERNAL)
getter(FunSpec.getterBuilder().apply {
addAnnotation(AnnotationSpec.builder(JvmName::class).addMember("\"$getterKey\"").build())
addStatement("return $fixedKey()")
}.build())
}.build())
addFunction(FunSpec.builder(fixedKey).apply {
addKdoc("$kDoc\n@param formatArgs The format arguments that will be used for substitution")
addAnnotation(AnnotationSpec.builder(JvmName::class).addMember("\"$getterKey\"").build())
if (configs.isEnableRestrictedAccess) addModifiers(KModifier.INTERNAL)
addParameter(ParameterSpec.builder("formatArgs", Any::class.asTypeName()).addModifiers(KModifier.VARARG).build())
addStatement(statement)
returns(String::class)
}.build())
}
}.build())
}.build()
}.getOrElse { FError.make("Failed to generated Kotlin file\n$it") }
/**
* 转换为自动换行键值对数组
* @return [List]<[Pair]<[String], [String]>>
*/
private fun List<Pair<String, String>>.toAutoWrapKeyValues(): List<Pair<String, String>> {
val maxAllowLength = 75
val punctuations = charArrayOf('.', '。', ',', '', '、', ';', '', ':', '', '!', '', '?', '')
val result = mutableListOf<Pair<String, String>>()
val placeholders = mutableListOf<Pair<String, String>>()
forEach {
var key = it.first
var value = it.second.replace("\\n", "")
val maxLength = abs(maxAllowLength - key.length)
while (value.length > maxLength) {
var splitIndex = maxLength
var splitValue = value.substring(0, splitIndex)
val lastSpaceIndex = splitValue.lastIndexOf(' ')
val lastPunctuationIndex = splitValue.lastIndexOfAny(punctuations)
val hashWrapIndex = splitValue.lastIndexOf('')
when {
hashWrapIndex != -1 && (hashWrapIndex < lastSpaceIndex || hashWrapIndex < lastPunctuationIndex) -> {
splitIndex = hashWrapIndex
splitValue = value.substring(0, splitIndex)
}
lastSpaceIndex != -1 && lastSpaceIndex >= lastPunctuationIndex -> {
splitIndex = lastSpaceIndex + 1
splitValue = value.substring(0, splitIndex)
}
lastPunctuationIndex != -1 -> {
splitIndex = lastPunctuationIndex + 1
splitValue = value.substring(0, splitIndex)
}
}
value = value.substring(splitIndex).trimStart('')
result.add(key to splitValue)
key = " ".repeat(key.length)
}
if (value.isNotEmpty())
result.add(key to value.replace("", ""))
else placeholders.add(key to "")
}; result.addAll(placeholders)
return result
}
/**
* 转换到 KotlinPoet 声明的空格
* @return [String]
*/
private fun String.toKotlinPoetSpace() = replace(" ", "·")
}

View File

@@ -0,0 +1,33 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/13.
*/
package com.highcapable.flexilocale.plugin.generator.factory
import java.io.File
/** I18ns 数组类型定义 */
internal typealias LocaleStringMap = MutableMap<String, LocaleChildMap>
/** I18ns (子键值对) 数组类型定义 */
internal typealias LocaleChildMap = MutableMap<String, String>
/** I18ns (文件) 数组类型定义 */
internal typealias LocaleFileMap = MutableMap<String, MutableSet<File>>

View File

@@ -0,0 +1,216 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/10.
*/
@file:Suppress("DEPRECATION")
package com.highcapable.flexilocale.plugin.helper
import com.android.build.gradle.AppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.api.BaseVariant
import com.highcapable.flexilocale.gradle.factory.get
import com.highcapable.flexilocale.plugin.config.proxy.IFlexiLocaleConfigs
import com.highcapable.flexilocale.plugin.generator.LocaleSourcesGenerator
import com.highcapable.flexilocale.plugin.generator.factory.LocaleChildMap
import com.highcapable.flexilocale.plugin.generator.factory.LocaleFileMap
import com.highcapable.flexilocale.plugin.generator.factory.LocaleStringMap
import com.highcapable.flexilocale.utils.debug.FError
import com.highcapable.flexilocale.utils.debug.FLog
import com.highcapable.flexilocale.utils.factory.toFile
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import org.w3c.dom.Element
import org.w3c.dom.Node
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
/**
* I18ns 分析工具类
*/
internal object LocaleAnalysisHelper {
/** Android 的 Application 插件名称 */
private const val APPLICATION_PLUGIN_NAME = "com.android.application"
/** Android 的 Library 插件名称 */
private const val LIBRARY_PLUGIN_NAME = "com.android.library"
/** Kotlin 的 Android 插件名称 */
private const val KT_ANDROID_PLUGIN_NAME = "org.jetbrains.kotlin.android"
/** I18ns 代码生成实例 */
private val generator = LocaleSourcesGenerator()
/** 当前全部 I18ns 数据 (来自但不一定完全为 strings.xml) */
private val mappedStrings: LocaleStringMap = mutableMapOf()
/** 当前项目命名空间 */
private var namespace = ""
/** 当前项目资源目录数组 */
private val resDirectories = mutableListOf<File>()
/** 上次修改的 Hash Code */
private var lastModifiedHashCode = 0
/** 配置是否已被修改 */
private var isConfigsModified = true
/** 当前使用的配置实例 */
private lateinit var configs: IFlexiLocaleConfigs
/**
* 开始分析当前项目
* @param project 当前项目
* @param configs 当前配置
*/
internal fun start(project: Project, configs: IFlexiLocaleConfigs) {
this.configs = configs
if (!configs.isEnable) return
checkingConfigsModified(project, configs)
initializePlugins(project)
val lastMappedStrings: LocaleStringMap = mutableMapOf()
val lastResolveStrings: LocaleStringMap = mutableMapOf()
resDirectories.takeIf { it.isNotEmpty() }?.allValuesDirs()?.forEach { (localeName, files) ->
val stringXmls: LocaleChildMap = mutableMapOf()
files.forEach { stringXmls.putAll(resolveStringXml(it)) }
lastResolveStrings[localeName] = stringXmls
} ?: return FLog.warn(
"Unable to get the resources dir of $project, " +
"please check whether there does not have a resources dir or is not an Android project"
)
lastResolveStrings.onEach { (localeName, strings) ->
strings.forEach { (key, value) ->
if (lastMappedStrings[key] == null) lastMappedStrings[key] = mutableMapOf()
lastMappedStrings[key]?.set(localeName, value)
}
}.clear()
val isFileModified = mappedStrings != lastMappedStrings
if (!isFileModified && !isConfigsModified) return
mappedStrings.clear()
mappedStrings.putAll(lastMappedStrings)
lastMappedStrings.clear()
updateGeneration()
}
/**
* 检查配置是否已被修改
* @param project 当前项目
* @param configs 当前配置
*/
private fun checkingConfigsModified(project: Project, configs: IFlexiLocaleConfigs) {
val fileHashCode = project.buildFile.takeIf { it.exists() }?.readText()?.hashCode() ?: -1
isConfigsModified = fileHashCode == -1 || lastModifiedHashCode != fileHashCode || this.configs.innerHashCode() != configs.innerHashCode()
lastModifiedHashCode = fileHashCode
}
/**
* 初始化 Android Gradle plugin
* @param project 当前项目
*/
private fun initializePlugins(project: Project) {
runCatching {
fun BaseExtension.updateSourceDirs() = sourceSets.configureEach { kotlin.srcDir(configs.generateDirPath) }
fun KotlinProjectExtension.updateSourceDirs() = sourceSets.configureEach { kotlin.srcDir(configs.generateDirPath) }
fun BaseVariant.updateResDirectories() = sourceSets.forEach { provide -> provide.resDirectories?.also { resDirectories.addAll(it) } }
project.plugins.withId(APPLICATION_PLUGIN_NAME) {
project.get<AppExtension>().also { extension ->
namespace = extension.namespace ?: ""
extension.applicationVariants.forEach { variant ->
variant.updateResDirectories()
}; extension.updateSourceDirs()
}
}
project.plugins.withId(LIBRARY_PLUGIN_NAME) {
project.get<LibraryExtension>().also { extension ->
namespace = extension.namespace ?: ""
extension.libraryVariants.forEach { variant ->
variant.updateResDirectories()
}; extension.updateSourceDirs()
}
}
project.plugins.withId(KT_ANDROID_PLUGIN_NAME) {
project.get<KotlinAndroidProjectExtension>().also { extension ->
extension.updateSourceDirs()
}
}
}.onFailure { FError.make("Failed to initialize Android Gradle plugin, this may be not or a wrong Android project\n$it") }
}
/** 更新生成后的代码内容 */
private fun updateGeneration() {
val packageName = "${configs.packageName.ifBlank { namespace }}.generated.locale"
val generateDir = configs.generateDirPath.toFile().apply { if (exists() && isDirectory) deleteRecursively() }
generator.build(configs, mappedStrings, namespace, packageName).writeTo(generateDir)
}
/**
* 解析当前资源目录下的全部可用 values 目录数组 (包含 I18ns 数据)
* @return [LocaleFileMap]
*/
private fun List<File>.allValuesDirs(): LocaleFileMap {
val valuesDirs: LocaleFileMap = mutableMapOf()
forEach {
it.listFiles()?.filter { dir -> dir.name.startsWith("values") }?.forEach eachDir@{ valuesDir ->
if (!valuesDir.exists() || !valuesDir.isDirectory) return@eachDir
val langName = if (valuesDir.name == "values") "default" else valuesDir.name.split("s-").getOrNull(1) ?: return@eachDir
if (valuesDirs[langName] == null) valuesDirs[langName] = mutableSetOf()
valuesDirs[langName]?.add(valuesDir)
}
}; return valuesDirs
}
/**
* 解析当前资源目录下的全部 Xml 文件内容到键值对数组
* @param valuesDir 当前资源目录
* @return [LocaleChildMap]
*/
private fun resolveStringXml(valuesDir: File): LocaleChildMap {
val lastMappedStrings: LocaleChildMap = mutableMapOf()
valuesDir.listFiles()?.filter { it.name.endsWith(".xml") }?.forEach {
lastMappedStrings.putAll(it.readText().parseResourcesXml())
}; return lastMappedStrings
}
/**
* 解析资源 Xml 文件内容到键值对数组
* @return [LocaleChildMap]
*/
private fun String.parseResourcesXml(): LocaleChildMap {
val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
val document = runCatching { builder.parse(byteInputStream()) }.getOrNull() ?: return mutableMapOf()
val rootNode = document.documentElement
if (rootNode.nodeName != "resources") return mutableMapOf()
val nodes = rootNode.getElementsByTagName("string")
val keyValues: LocaleChildMap = mutableMapOf()
(0 until nodes.length).forEach { index ->
val node = nodes.item(index)
if (node.nodeType == Node.ELEMENT_NODE) {
val element = node as Element
val name = element.getAttribute("name")
val content = element.textContent
keyValues[name] = content
}
}; return keyValues
}
}

View File

@@ -0,0 +1,37 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/10.
*/
package com.highcapable.flexilocale.utils.debug
import com.highcapable.flexilocale.FlexiLocale
/**
* 全局异常管理类
*/
internal object FError {
/**
* 抛出异常
* @param msg 消息内容
* @throws IllegalStateException
*/
internal fun make(msg: String): Nothing = error("[${FlexiLocale.TAG}] $msg")
}

View File

@@ -0,0 +1,103 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/10.
*/
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package com.highcapable.flexilocale.utils.debug
import com.highcapable.flexilocale.FlexiLocale
import org.apache.log4j.Logger
/**
* 全局 Log 管理类
*/
internal object FLog {
internal const val DONE = ""
internal const val IGNORE = ""
internal const val ERROR = ""
internal const val WARN = "⚠️"
internal const val LINK = "➡️"
internal const val WIRE = "⚙️"
internal const val UP = "⬆️"
internal const val ROTATE = "\uD83D\uDD04"
internal const val ANLZE = "\uD83D\uDD0D"
internal const val STRNG = "\uD83D\uDCAA"
/** 当前日志输出对象 */
private val logger = Logger.getLogger(FLog::class.java)
/**
* 打印 Info (提醒) 级别 Log (绿色)
* @param msg 消息内容
* @param symbol 前缀符号 - 仅限非 [noTag] - 默认无
* @param noTag 无标签 - 默认否
*/
internal fun note(msg: Any, symbol: String = "", noTag: Boolean = false) =
log(if (noTag) msg else msg.createSymbolMsg(symbol), color = "38;5;10")
/**
* 打印 Info 级别 Log (无颜色)
* @param msg 消息内容
* @param symbol 前缀符号 - 仅限非 [noTag] - 默认无
* @param noTag 无标签 - 默认否
*/
internal fun info(msg: Any, symbol: String = "", noTag: Boolean = false) =
log(if (noTag) msg else msg.createSymbolMsg(symbol))
/**
* 打印 Warn 级别 Log (黄色)
* @param msg 消息内容
* @param symbol 前缀符号 - 仅限非 [noTag] - 默认 [WARN]
* @param noTag 无标签 - 默认否
*/
internal fun warn(msg: Any, symbol: String = WARN, noTag: Boolean = false) =
log(if (noTag) msg else msg.createSymbolMsg(symbol), color = "33")
/**
* 打印 Error 级别 Log (红色)
* @param msg 消息内容
* @param symbol 前缀符号 - 仅限非 [noTag] - 默认 [ERROR]
* @param noTag 无标签 - 默认否
*/
internal fun error(msg: Any, symbol: String = ERROR, noTag: Boolean = false) =
log(if (noTag) msg else msg.createSymbolMsg(symbol), isError = true)
/**
* 创建符号消息内容
* @param symbol 前缀符号
* @return [String]
*/
private fun Any.createSymbolMsg(symbol: String) =
if (symbol.isNotBlank()) "[${FlexiLocale.TAG}] $symbol $this" else "[${FlexiLocale.TAG}] $this"
/**
* 打印 Log
* @param msg 消息内容
* @param color 颜色代码 - 默认无颜色
* @param isError 是否强制为错误日志 - 默认否
*/
private fun log(msg: Any, color: String = "0", isError: Boolean = false) = when {
isError -> logger.error(msg)
color != "0" -> println("\u001B[${color}m$msg\u001B[0m")
else -> println(msg)
}
}

View File

@@ -0,0 +1,40 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/11.
*/
@file:Suppress("unused")
package com.highcapable.flexilocale.utils.factory
import java.io.File
/**
* 字符串路径转换为文件
*
* 自动调用 [parseFileSeparator]
* @return [File]
*/
internal fun String.toFile() = File(parseFileSeparator())
/**
* 格式化到当前操作系统的文件分隔符
* @return [String]
*/
internal fun String.parseFileSeparator() = replace("/", File.separator).replace("\\", File.separator)

View File

@@ -0,0 +1,44 @@
/*
* FlexiLocale - An easy generation Android i18ns string call Gradle plugin.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiLocale
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2023/10/10.
*/
package com.highcapable.flexilocale.utils.factory
/**
* 下划线、分隔线、点、冒号、空格命名字符串转小驼峰命名字符串
* @return [String]
*/
internal fun String.camelcase() = runCatching {
split("_", ".", "-", ":", " ").map { it.replaceFirstChar { e -> e.titlecase() } }.let { words ->
words.first().replaceFirstChar { it.lowercase() } + words.drop(1).joinToString("")
}
}.getOrNull() ?: this
/**
* 下划线、分隔线、点、空格命名字符串转大驼峰命名字符串
* @return [String]
*/
internal fun String.uppercamelcase() = camelcase().capitalize()
/**
* 字符串首字母大写
* @return [String]
*/
internal fun String.capitalize() = replaceFirstChar { it.uppercaseChar() }