Skip to content

Commit

Permalink
Translation: option to force json and configurable default i18n call
Browse files Browse the repository at this point in the history
  • Loading branch information
Moulberry authored and RedNesto committed Jul 7, 2024
1 parent caca374 commit 336924b
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 11 deletions.
71 changes: 71 additions & 0 deletions src/main/kotlin/TranslationSettings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Minecraft Development for IntelliJ
*
* https://mcdev.io/
*
* Copyright (C) 2024 minecraft-dev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, version 3.0 only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.demonwav.mcdev

import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project

@State(name = "TranslationSettings", storages = [Storage("minecraft_dev.xml")])
class TranslationSettings : PersistentStateComponent<TranslationSettings.State> {

data class State(
var isForceJsonTranslationFile: Boolean = false,
var isUseCustomConvertToTranslationTemplate: Boolean = false,
var convertToTranslationTemplate: String = "net.minecraft.client.resources.I18n.format(\"\$key\")",
)

private var state = State()

override fun getState(): State {
return state
}

override fun loadState(state: State) {
this.state = state
}

// State mappings
var isForceJsonTranslationFile: Boolean
get() = state.isForceJsonTranslationFile
set(forceJsonTranslationFile) {
state.isForceJsonTranslationFile = forceJsonTranslationFile
}

var isUseCustomConvertToTranslationTemplate: Boolean
get() = state.isUseCustomConvertToTranslationTemplate
set(useCustomConvertToTranslationTemplate) {
state.isUseCustomConvertToTranslationTemplate = useCustomConvertToTranslationTemplate
}

var convertToTranslationTemplate: String
get() = state.convertToTranslationTemplate
set(convertToTranslationTemplate) {
state.convertToTranslationTemplate = convertToTranslationTemplate
}

companion object {
@JvmStatic
fun getInstance(project: Project): TranslationSettings = project.service()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ object HardcodedYarnToMojmap {
owner = "net.minecraft.network.chat.Component",
name = "translatableEscape",
descriptor = "(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;"
),
MemberReference(
owner = "net.minecraft.client.resource.language.I18n",
name = "translate",
descriptor = "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;"
) mapTo MemberReference(
owner = "net.minecraft.client.resources.language.I18n",
name = "get",
descriptor = "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;"
)
),
hashMapOf(),
Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/platform/mcp/mappings/Mappings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ fun Module.getMappedMethod(mojangClass: String, mojangMethod: String, mojangDesc
return getMappedMethod(MemberReference(mojangMethod, mojangDescriptor, mojangClass))
}

fun Module.getMappedMethodCall(mojangClass: String, mojangMethod: String, mojangDescriptor: String, p: String): String {
val mappedMethodRef = namedToMojang?.tryGetMappedMethod(
MemberReference(mojangMethod, mojangDescriptor, mojangClass)
) ?: return "$mojangClass.$mojangMethod($p)"
return "${mappedMethodRef.owner}.${mappedMethodRef.name}($p)"
}

fun Module.getMojangMethod(mappedMethod: MemberReference): String {
return namedToMojang?.getIntermediaryMethod(mappedMethod)?.name ?: return mappedMethod.name
}
Expand Down
54 changes: 48 additions & 6 deletions src/main/kotlin/translations/TranslationFiles.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

package com.demonwav.mcdev.translations

import com.demonwav.mcdev.TranslationSettings
import com.demonwav.mcdev.translations.index.TranslationIndex
import com.demonwav.mcdev.translations.index.TranslationInverseIndex
import com.demonwav.mcdev.translations.lang.LangFile
Expand Down Expand Up @@ -110,12 +111,43 @@ object TranslationFiles {
element.delete()
}

fun findTranslationKeyForText(context: PsiElement, text: String): String? {
val module = context.findModule()
?: throw IllegalArgumentException("Cannot add translation for element outside of module")
var jsonVersion = true
if (!TranslationSettings.getInstance(context.project).isForceJsonTranslationFile) {
val version =
context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context")
jsonVersion = version > MC_1_12_2
}

if (!jsonVersion) {
// This feature only supports JSON translation files
return null
}

val files = FileTypeIndex.getFiles(
JsonFileType.INSTANCE,
GlobalSearchScope.moduleScope(module),
).filter { getLocale(it) == TranslationConstants.DEFAULT_LOCALE }

for (file in files) {
val psiFile = PsiManager.getInstance(context.project).findFile(file) ?: continue
psiFile.findKeyForTextAsJson(text)?.let { return it }
}

return null
}

fun add(context: PsiElement, key: String, text: String) {
val module = context.findModule()
?: throw IllegalArgumentException("Cannot add translation for element outside of module")
val version =
context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context")
val jsonVersion = version > MC_1_12_2
var jsonVersion = true
if (!TranslationSettings.getInstance(context.project).isForceJsonTranslationFile) {
val version =
context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context")
jsonVersion = version > MC_1_12_2
}

fun write(files: Iterable<VirtualFile>) {
for (file in files) {
Expand Down Expand Up @@ -223,6 +255,13 @@ object TranslationFiles {
doc.insertString(rootObject.lastChild.prevSibling.textOffset, content)
}

private fun PsiFile.findKeyForTextAsJson(text: String): String? {
val rootObject = this.firstChild as? JsonObject ?: return null
return rootObject.propertyList.firstOrNull {
(it.value as? JsonStringLiteral)?.value == text
}?.name
}

private fun generateJsonFile(
leadingComma: Boolean,
indent: CharSequence,
Expand Down Expand Up @@ -292,9 +331,12 @@ object TranslationFiles {
fun buildSortingTemplateFromDefault(context: PsiElement, domain: String? = null): Template? {
val module = context.findModule()
?: throw IllegalArgumentException("Cannot add translation for element outside of module")
val version =
context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context")
val jsonVersion = version > MC_1_12_2
var jsonVersion = true
if (!TranslationSettings.getInstance(context.project).isForceJsonTranslationFile) {
val version =
context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context")
jsonVersion = version > MC_1_12_2
}

val defaultTranslationFile = FileBasedIndex.getInstance()
.getContainingFiles(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@

package com.demonwav.mcdev.translations.intentions

import com.demonwav.mcdev.TranslationSettings
import com.demonwav.mcdev.platform.mcp.mappings.getMappedMethodCall
import com.demonwav.mcdev.translations.TranslationFiles
import com.demonwav.mcdev.util.findModule
import com.demonwav.mcdev.util.runWriteAction
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction
import com.intellij.lang.java.JavaLanguage
Expand All @@ -42,14 +45,17 @@ class ConvertToTranslationIntention : PsiElementBaseIntentionAction() {
override fun invoke(project: Project, editor: Editor, element: PsiElement) {
if (element.parent is PsiLiteral) {
val value = (element.parent as PsiLiteral).value as? String ?: return

val existingKey = TranslationFiles.findTranslationKeyForText(element, value)

val result = Messages.showInputDialogWithCheckBox(
"Enter translation key:",
"Convert String Literal to Translation",
"Replace literal with call to I18n (only works on clients!)",
true,
true,
Messages.getQuestionIcon(),
null,
existingKey,
object : InputValidatorEx {
override fun getErrorText(inputString: String): String? {
if (inputString.isEmpty()) {
Expand All @@ -73,12 +79,24 @@ class ConvertToTranslationIntention : PsiElementBaseIntentionAction() {
val key = result.first ?: return
val replaceLiteral = result.second
try {
TranslationFiles.add(element, key, value)
if (existingKey != key) {
TranslationFiles.add(element, key, value)
}
if (replaceLiteral) {
val translationSettings = TranslationSettings.getInstance(project)
val psi = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return
psi.runWriteAction {
val expression = JavaPsiFacade.getElementFactory(project).createExpressionFromText(
"net.minecraft.client.resources.I18n.format(\"$key\")",
if (translationSettings.isUseCustomConvertToTranslationTemplate) {
translationSettings.convertToTranslationTemplate.replace("\$key", key)
} else {
element.findModule()?.getMappedMethodCall(
"net.minecraft.client.resource.language.I18n",
"translate",
"(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;",
"\"$key\""
) ?: "net.minecraft.client.resource.I18n.get(\"$key\")"
},
element.context,
)
if (psi.language === JavaLanguage.INSTANCE) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

package com.demonwav.mcdev.translations.sorting

import com.demonwav.mcdev.TranslationSettings
import com.demonwav.mcdev.asset.MCDevBundle
import com.demonwav.mcdev.translations.lang.colors.LangSyntaxHighlighter
import com.intellij.codeInsight.template.impl.TemplateEditorUtil
Expand All @@ -32,7 +33,13 @@ import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.project.Project
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.COLUMNS_LARGE
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.columns
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.selected
import com.intellij.ui.layout.ComponentPredicate
import com.intellij.util.ui.JBUI
import java.awt.BorderLayout
import javax.swing.DefaultComboBoxModel
Expand All @@ -46,7 +53,7 @@ class TranslationTemplateConfigurable(private val project: Project) : Configurab
private var templateEditor: Editor? = null

private val editorPanel = JPanel(BorderLayout()).apply {
preferredSize = JBUI.size(250, 450)
preferredSize = JBUI.size(250, 350)
minimumSize = preferredSize
}

Expand All @@ -62,6 +69,25 @@ class TranslationTemplateConfigurable(private val project: Project) : Configurab
row {
cell(editorPanel).align(Align.FILL)
}

val translationSettings = TranslationSettings.getInstance(project)
row {
checkBox(MCDevBundle("minecraft.settings.translation.force_json_translation_file"))
.bindSelected(translationSettings::isForceJsonTranslationFile)
}

lateinit var allowConvertToTranslationTemplate: ComponentPredicate
row {
val checkBox = checkBox(MCDevBundle("minecraft.settings.translation.use_custom_convert_template"))
.bindSelected(translationSettings::isUseCustomConvertToTranslationTemplate)
allowConvertToTranslationTemplate = checkBox.selected
}

row {
textField().bindText(translationSettings::convertToTranslationTemplate)
.enabledIf(allowConvertToTranslationTemplate)
.columns(COLUMNS_LARGE)
}
}

@Nls
Expand Down Expand Up @@ -107,7 +133,7 @@ class TranslationTemplateConfigurable(private val project: Project) : Configurab
}

override fun isModified(): Boolean {
return templateEditor?.document?.text != getActiveTemplateText() != false
return templateEditor?.document?.text != getActiveTemplateText() != false || panel.isModified()
}

override fun apply() {
Expand All @@ -120,9 +146,12 @@ class TranslationTemplateConfigurable(private val project: Project) : Configurab
} else if (project != null) {
TemplateManager.writeProjectTemplate(project, editor.document.text)
}

panel.apply()
}

override fun reset() {
init()
panel.reset()
}
}
1 change: 1 addition & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@
<externalAnnotationsArtifactsResolver implementation="com.demonwav.mcdev.translations.identification.TranslationExternalAnnotationsArtifactsResolver" order="first"/>

<applicationService serviceImplementation="com.demonwav.mcdev.MinecraftSettings"/>
<projectService serviceImplementation="com.demonwav.mcdev.TranslationSettings"/>

<errorHandler implementation="com.demonwav.mcdev.errorreporter.ErrorReporter"/>
<!--endregion-->
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/messages/MinecraftDevelopment.properties
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,6 @@ minecraft.settings.lang_template.project_must_be_selected=You must have selected
minecraft.settings.lang_template.comment=<html>You may edit the template used for translation key sorting here.\
<br> Each line may be empty, a comment (with <font face="monospace">#</font>) or a glob pattern for matching translation keys (like <font face="monospace">"item.*"</font>).\
<br> <b>Note: Empty lines are respected and will be put into the sorting result.</b></html>
minecraft.settings.translation=Translation
minecraft.settings.translation.force_json_translation_file=Force JSON translation file (1.13+)
minecraft.settings.translation.use_custom_convert_template=Use custom template for convert literal to translation

0 comments on commit 336924b

Please sign in to comment.