Skip to content

Commit

Permalink
Support translating name & description
Browse files Browse the repository at this point in the history
Move name & description definition from `gradle.properties` to a lang file in `:data`.

Introduce `ModNameProcessor` & `ModDescriptionProcessor`, along with a new method `LangTask.getTranslation()` which can be used to get a specific translation after the task has finished running.

Make use of this in `:data` to get the name & description from the translations into the mod metadata file.

Fixes MinecraftFreecam#172
  • Loading branch information
MattSturgeon committed Feb 17, 2024
1 parent 9a3786b commit 2eab327
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 29 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and Freecam's versioning is based on [Semantic Versioning](https://semver.org/sp

### Added

- \[Fabric]: The name & description shown in the Mod Menu can now be translated ([#172](https://github.com/MinecraftFreecam/Freecam/issues/172)).

### Changed

### Removed
Expand Down
30 changes: 29 additions & 1 deletion buildSrc/src/main/kotlin/net/xolt/freecam/gradle/LangTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ abstract class LangTask : DefaultTask() {
@TaskAction
fun build() {
val processors = listOf(
VariantTooltipProcessor(variant.get())
VariantTooltipProcessor(variant.get()),
ModDescriptionProcessor(modId.get(), variant.get()),
ModNameProcessor(modId.get(), variant.get())
)

val languages = inputDirectory.get().asFile
Expand All @@ -91,6 +93,32 @@ abstract class LangTask : DefaultTask() {
}
}

/**
* Get the given translation, for the given language.
*
* Will fall back to using the [source language][source] if the key isn't
* found in the specified language or if language isn't specified.
*
* Should only be used **after** this task has finished executing.
* I.e. **not** during Gradle's configuration step.
*
* @param key the translation key
* @param lang the locale, e.g. en-US, en_us, or zh-CN
* @return the translation, or null if not found
*/
@JvmOverloads
fun getTranslation(key: String, lang: String = source.get()): String? {
val file = fileFor(lang)
val translation = readJsonFile(file)[key]

// Check "source" translation if key wasn't found
return if (translation == null && file != fileFor(source.get())) {
getTranslation(key)
} else {
translation
}
}

private fun fileFor(lang: String) = outputDirectory.get().asFile
.resolve("assets")
.resolve(modId.get())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package net.xolt.freecam.gradle

internal class ModDescriptionProcessor(private val modID: String, private val variant: String) : LangProcessor {
override fun process(
translations: Map<String, String>,
fallback: Map<String, String>?
): Map<String, String> {
val firstID = "${modID}.description"
val secondID = "${modID}.description.${variant}"
val ids = listOf(firstID, secondID)

// Nothing to do if this language has no "description" translations
if (ids.none(translations.keys::contains)) {
return translations
}

val map = translations.toMutableMap()

// Remove any description.variant keys
map.keys
.filter { it.startsWith("${firstID}.") }
.forEach(map::remove)

// Set modmenu summary if this language has a translation for firstID
translations[firstID]?.let { map["modmenu.summaryTranslation.${modID}"] = it }

// Set "full" description
// Use fallback if either part is missing from this language
ids.mapNotNull { translations[it] ?: fallback?.get(it) }
.joinToString(" ")
.let { description ->
map[firstID] = description
map["modmenu.descriptionTranslation.${modID}"] = description
}

return map
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package net.xolt.freecam.gradle

internal class ModNameProcessor(private val modID: String, private val variant: String) : LangProcessor {
override fun process(
translations: Map<String, String>,
fallback: Map<String, String>?
): Map<String, String> {
val firstID = "${modID}.name"
val secondID = "${modID}.name.${variant}"
val ids = listOf(firstID, secondID)

// Nothing to do if this language has no "name" translations
if (ids.none(translations.keys::contains)) {
return translations
}

val map = translations.toMutableMap()

// Remove any name.variant keys
map.keys
.filter { it.startsWith("${firstID}.") }
.forEach(map::remove)

// Set "full" name
// Use fallback if either part is missing from this language
ids.mapNotNull { translations[it] ?: fallback?.get(it) }
.joinToString(" ")
.let { name ->
map[firstID] = name
map["modmenu.nameTranslation.${modID}"] = name
}

return map
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package net.xolt.freecam.gradle

import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.TestFactory

class ModDescriptionProcessorTest {

@TestFactory
fun `Basic tests`(): List<DynamicTest> {
val sample1 = mapOf(
"modid.description" to "Default description",
"modid.description.special" to "(extra special)"
)

return listOf(
processorTest(
name = "Discard variant descriptions",
processor = ModDescriptionProcessor("modid", "normal"),
translations = sample1,
result = mapOf(
"modid.description" to "Default description",
"modmenu.descriptionTranslation.modid" to "Default description",
"modmenu.summaryTranslation.modid" to "Default description"
)
),
processorTest(
name = "Append \"extra special\" to description",
processor = ModDescriptionProcessor("modid", "special"),
translations = sample1,
result = mapOf(
"modid.description" to "Default description (extra special)",
"modmenu.descriptionTranslation.modid" to "Default description (extra special)",
"modmenu.summaryTranslation.modid" to "Default description"
)
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package net.xolt.freecam.gradle

import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.TestFactory

class ModNameProcessorTest {

@TestFactory
fun `Basic tests`(): List<DynamicTest> {
val sample1 = mapOf(
"modid.name" to "ModName",
"modid.name.special" to "(extra special)"
)

return listOf(
processorTest(
name = "Discard variant names",
processor = ModNameProcessor("modid", "normal"),
translations = sample1,
result = mapOf(
"modid.name" to "ModName",
"modmenu.nameTranslation.modid" to "ModName"
)
),
processorTest(
name = "Append \"extra special\" to name",
processor = ModNameProcessor("modid", "special"),
translations = sample1,
result = mapOf(
"modid.name" to "ModName (extra special)",
"modmenu.nameTranslation.modid" to "ModName (extra special)"
)
)
)
}
}
55 changes: 30 additions & 25 deletions data/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,18 @@ variants.each { variant ->
kebabName += "-$variant"
}

def metadataTask = tasks.register("${name}GenMetadata", Copy) {
def metadataTask = tasks.register("${name}GenMetadata") {
group = "metadata"
description = {
def file = platform.endsWith "forge" ? "mods.toml" : "${platform}.mods.json"
return "Build $variant $file file"
}()

// Include files under `src/platform`
from layout.projectDirectory.dir("src").dir(platform)
into layout.buildDirectory.dir("metadata/${kebabName}")

// Depend on langTask and the following properties
dependsOn langTask
inputs.properties(
mod_id: rootProject.name,
modrinth_name: project.modrinth_name,
version: project.mod_version,
authors: project.authors,
description: project.description,
modrinth_description: project.modrinth_description,
licence: project.licence,
homepage_url: project.homepage_url,
source_code_url: project.source_code_url,
Expand All @@ -54,22 +48,33 @@ variants.each { variant ->
neoforge_req: project.neoforge_req,
)

def overrides = new TreeMap()
overrides.mod_id = inputs.properties.mod_id.toLowerCase()
overrides.name = overrides.mod_id.capitalize()
overrides.json_authors = inputs.properties.authors.split(',').collect { "\"$it\"" }.join(", ")

if (variant == "modrinth") {
overrides.name += ' ' + inputs.properties.modrinth_name
overrides.description = inputs.properties.description + ' ' + inputs.properties.modrinth_description
}

filesMatching("fabric.mod.json") {
expand inputs.properties + overrides
}

filesMatching("META-INF/mods.toml") {
expand inputs.properties + overrides
def inputDir = layout.projectDirectory.dir("src").dir(platform)
def outputDir = layout.buildDirectory.dir("metadata/${kebabName}")

// Declare task outputs so they can be used by other tasks
outputs.dir outputDir

// Need to do this at execution time;
// LangTask.getTranslation() can't be used at configuration time...
doLast {
def values = new TreeMap(inputs.properties)
values.mod_id = rootProject.name.toLowerCase()
values.name = langTask.get().getTranslation("freecam.name")
values.description = langTask.get().getTranslation("freecam.description")
values.json_authors = values.authors.split(',').collect { "\"$it\"" }.join(", ")

copy {
from inputDir
into outputDir

filesMatching("fabric.mod.json") {
expand values
}

filesMatching("META-INF/mods.toml") {
expand values
}
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions data/src/lang/en-US/mod.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"freecam.name": "Freecam",
"freecam.name.modrinth": "(Modrinth Edition)",
"freecam.description": "A highly customizable freecam mod.",
"freecam.description.modrinth": "Some features have been restricted to comply with Modrinth's Content Rules."
}
3 changes: 0 additions & 3 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ org.gradle.jvmargs=-Xmx4G
mod_version=1.2.3
maven_group=net.xolt.freecam
authors=hashalite,Matt Sturgeon
description=A highly customizable freecam mod.
modrinth_name=(Modrinth Edition)
modrinth_description=Some features have been restricted to comply with Modrinth's Content Rules.
licence=MIT
homepage_url=https://www.curseforge.com/minecraft/mc-mods/free-cam
source_code_url=https://github.com/MinecraftFreecam/Freecam
Expand Down

0 comments on commit 2eab327

Please sign in to comment.