Skip to content

Commit 9cf229c

Browse files
committed
Add handling for translations in deprecated.json. Closes #2511
1 parent 63357e4 commit 9cf229c

File tree

7 files changed

+198
-15
lines changed

7 files changed

+198
-15
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2025 minecraft-dev
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, version 3.0 only.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.demonwav.mcdev.translations
22+
23+
import com.google.gson.JsonParser
24+
import com.intellij.openapi.project.Project
25+
import com.intellij.openapi.roots.OrderEnumerator
26+
import com.intellij.openapi.roots.ProjectRootModificationTracker
27+
import com.intellij.openapi.vfs.VirtualFile
28+
import com.intellij.psi.util.CachedValueProvider
29+
import com.intellij.psi.util.CachedValuesManager
30+
import java.io.InputStreamReader
31+
import java.io.IOException
32+
import java.nio.charset.StandardCharsets
33+
34+
class DeprecatedTranslations private constructor(val removed: Set<String>, val renamed: Map<String, String>) {
35+
val inverseRenamed = renamed.entries.associateBy({ it.value }) { it.key }
36+
37+
val isEmpty
38+
get() = removed.isEmpty() && renamed.isEmpty()
39+
40+
companion object {
41+
private val DEFAULT = DeprecatedTranslations(emptySet(), emptyMap())
42+
private const val FILE_PATH = "assets/minecraft/lang/deprecated.json"
43+
44+
fun getInstance(project: Project): DeprecatedTranslations {
45+
return CachedValuesManager.getManager(project).getCachedValue(project) {
46+
val file = findFile(project) ?: return@getCachedValue CachedValueProvider.Result(
47+
DEFAULT,
48+
ProjectRootModificationTracker.getInstance(project)
49+
)
50+
CachedValueProvider.Result(getInstanceFromFile(file), file)
51+
}
52+
}
53+
54+
private fun findFile(project: Project): VirtualFile? {
55+
for (libraryRoot in OrderEnumerator.orderEntries(project).librariesOnly().classes().roots) {
56+
val file = libraryRoot.findFileByRelativePath(FILE_PATH) ?: continue
57+
if (!file.isDirectory) {
58+
return file
59+
}
60+
}
61+
62+
return null
63+
}
64+
65+
private fun getInstanceFromFile(file: VirtualFile): DeprecatedTranslations {
66+
try {
67+
val rootElement = InputStreamReader(file.inputStream, StandardCharsets.UTF_8).use { reader ->
68+
JsonParser.parseReader(reader)
69+
}
70+
71+
if (rootElement == null || !rootElement.isJsonObject) {
72+
return DEFAULT
73+
}
74+
75+
val obj = rootElement.asJsonObject
76+
77+
val removedElt = obj.get("removed")
78+
val removed = if (removedElt != null && removedElt.isJsonArray) {
79+
val removedArr = removedElt.asJsonArray
80+
removedArr.mapNotNullTo(HashSet.newHashSet(removedArr.size())) { e ->
81+
if (e.isJsonPrimitive && e.asJsonPrimitive.isString) {
82+
e.asString
83+
} else {
84+
null
85+
}
86+
}
87+
} else {
88+
emptySet()
89+
}
90+
91+
val renamedElt = obj.get("renamed")
92+
val renamed = if (renamedElt != null && renamedElt.isJsonObject) {
93+
val renamedObj = renamedElt.asJsonObject
94+
renamedObj.asMap().asSequence().mapNotNull { (k, v) ->
95+
if (v.isJsonPrimitive && v.asJsonPrimitive.isString) {
96+
k to v.asString
97+
} else {
98+
null
99+
}
100+
}.toMap(HashMap.newHashMap(renamedObj.size()))
101+
} else {
102+
emptyMap()
103+
}
104+
105+
return DeprecatedTranslations(removed, renamed)
106+
} catch (_: IOException) {
107+
return DEFAULT
108+
}
109+
}
110+
}
111+
}

src/main/kotlin/translations/TranslationFiles.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,6 @@ object TranslationFiles {
238238
is FileEntry.Comment -> result.append("# ${entry.text}\n")
239239
is FileEntry.Translation -> result.append("${entry.key}=${entry.text}\n")
240240
FileEntry.EmptyLine -> result.append('\n')
241-
// TODO: IntelliJ shows a false error here without the `else`. The compiler doesn't care because
242-
// FileEntry is a sealed class. When this bug in IntelliJ is fixed, remove this `else`.
243-
else -> {}
244241
}
245242
}
246243

@@ -282,9 +279,6 @@ object TranslationFiles {
282279
result.append("\"${StringUtil.escapeStringCharacters(entry.text)}\",\n")
283280
}
284281
FileEntry.EmptyLine -> result.append('\n')
285-
// TODO: IntelliJ shows a false error here without the `else`. The compiler doesn't care because
286-
// FileEntry is a sealed class. When this bug in IntelliJ is fixed, remove this `else`.
287-
else -> {}
288282
}
289283
}
290284

src/main/kotlin/translations/identification/TranslationIdentifier.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package com.demonwav.mcdev.translations.identification
2222

2323
import com.demonwav.mcdev.platform.mcp.mappings.getMappedClass
2424
import com.demonwav.mcdev.platform.mcp.mappings.getMappedMethod
25+
import com.demonwav.mcdev.translations.DeprecatedTranslations
2526
import com.demonwav.mcdev.translations.TranslationConstants
2627
import com.demonwav.mcdev.translations.identification.TranslationInstance.FormattingError
2728
import com.demonwav.mcdev.translations.index.TranslationIndex
@@ -89,10 +90,13 @@ object TranslationIdentifier {
8990
else -> element.evaluateString()
9091
}?.replace(CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED, "") ?: return null
9192

92-
val shouldFold = element is ULiteralExpression && element.isString
93+
val deprecations = DeprecatedTranslations.getInstance(project)
94+
val key = prefix + translationKey + suffix
95+
96+
val shouldFold = element is ULiteralExpression && element.isString && key !in deprecations.removed && key !in deprecations.renamed
9397

9498
val entries = TranslationIndex.getAllDefaultEntries(project).merge("")
95-
val translation = entries[prefix + translationKey + suffix]?.text
99+
val translation = entries[deprecations.inverseRenamed[key] ?: key]?.text
96100
?: return TranslationInstance( // translation doesn't exist
97101
null,
98102
index,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2025 minecraft-dev
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, version 3.0 only.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.demonwav.mcdev.translations.inspections
22+
23+
import com.demonwav.mcdev.translations.DeprecatedTranslations
24+
import com.demonwav.mcdev.translations.identification.TranslationIdentifier
25+
import com.intellij.codeInspection.ProblemsHolder
26+
import com.intellij.psi.PsiElementVisitor
27+
import com.intellij.uast.UastHintedVisitorAdapter
28+
import org.jetbrains.uast.UElement
29+
import org.jetbrains.uast.UExpression
30+
import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor
31+
32+
class DeprecatedTranslationInspection : TranslationInspection() {
33+
override fun getStaticDescription() = "Detects usage of translations that are removed or renamed in deprecated.json"
34+
35+
private val typesHint: Array<Class<out UElement>> = arrayOf(UExpression::class.java)
36+
37+
override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor =
38+
UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), typesHint)
39+
40+
private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() {
41+
val deprecations = DeprecatedTranslations.getInstance(holder.project)
42+
43+
override fun visitExpression(node: UExpression): Boolean {
44+
val result = TranslationIdentifier.identify(node)
45+
if (result != null && result.text != null) {
46+
val isRemoved = result.key.full in deprecations.removed
47+
val isRenamed = result.key.full in deprecations.renamed
48+
if (isRemoved || isRenamed) {
49+
val type = if (isRemoved) "removed" else "renamed"
50+
holder.registerProblem(
51+
node.sourcePsi!!,
52+
"Usage of $type translation in deprecated.json"
53+
)
54+
}
55+
}
56+
57+
return super.visitExpression(node)
58+
}
59+
}
60+
}

src/main/kotlin/translations/reference/TranslationGotoSymbolContributor.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
package com.demonwav.mcdev.translations.reference
2222

23+
import com.demonwav.mcdev.translations.DeprecatedTranslations
2324
import com.demonwav.mcdev.translations.index.TranslationIndex
2425
import com.demonwav.mcdev.translations.index.TranslationInverseIndex
2526
import com.demonwav.mcdev.translations.index.merge
@@ -57,7 +58,8 @@ class TranslationGotoSymbolContributor : ChooseByNameContributor {
5758
} else {
5859
GlobalSearchScope.projectScope(project)
5960
}
60-
val elements = TranslationInverseIndex.findElements(name, scope)
61+
val deprecations = DeprecatedTranslations.getInstance(project)
62+
val elements = TranslationInverseIndex.findElements(deprecations.inverseRenamed[name] ?: name, scope)
6163

6264
return elements.mapToArray { it as NavigationItem }
6365
}

src/main/kotlin/translations/reference/TranslationReference.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
package com.demonwav.mcdev.translations.reference
2222

2323
import com.demonwav.mcdev.asset.PlatformAssets
24+
import com.demonwav.mcdev.translations.DeprecatedTranslations
2425
import com.demonwav.mcdev.translations.TranslationConstants
2526
import com.demonwav.mcdev.translations.TranslationFiles
2627
import com.demonwav.mcdev.translations.identification.TranslationInstance
2728
import com.demonwav.mcdev.translations.index.TranslationIndex
2829
import com.demonwav.mcdev.translations.index.TranslationInverseIndex
2930
import com.demonwav.mcdev.translations.lang.gen.psi.LangEntry
31+
import com.demonwav.mcdev.util.filterNotNull
3032
import com.demonwav.mcdev.util.mapToArray
3133
import com.demonwav.mcdev.util.toTypedArray
3234
import com.intellij.codeInsight.lookup.LookupElementBuilder
@@ -52,8 +54,9 @@ class TranslationReference(
5254
) : PsiReferenceBase.Poly<PsiElement>(element, textRange, false), PsiPolyVariantReference {
5355
override fun multiResolve(incompleteCode: Boolean): Array<ResolveResult> {
5456
val project = myElement.project
57+
val deprecations = DeprecatedTranslations.getInstance(project)
5558
val entries = TranslationInverseIndex.findElements(
56-
key.full,
59+
deprecations.inverseRenamed[key.full] ?: key.full,
5760
GlobalSearchScope.allScope(project),
5861
TranslationConstants.DEFAULT_LOCALE,
5962
)
@@ -63,16 +66,18 @@ class TranslationReference(
6366
override fun getVariants(): Array<Any?> {
6467
val project = myElement.project
6568
val defaultTranslations = TranslationIndex.getAllDefaultTranslations(project)
69+
val deprecations = DeprecatedTranslations.getInstance(project)
6670
val pattern = Regex("${Regex.escape(key.prefix)}(.*?)${Regex.escape(key.suffix)}")
6771
return defaultTranslations
68-
.filter { it.key.isNotEmpty() }
69-
.mapNotNull { entry -> pattern.matchEntire(entry.key)?.let { entry to it } }
70-
.map { (entry, match) ->
72+
.map { it.key }
73+
.filter { it.isNotEmpty() && it !in deprecations.removed && it !in deprecations.renamed }
74+
.mapNotNull { key -> pattern.matchEntire(key)?.let { key to it } }
75+
.map { (key, match) ->
7176
LookupElementBuilder
72-
.create(if (match.groups.size <= 1) entry.key else match.groupValues[1])
77+
.create(if (match.groups.size <= 1) key else match.groupValues[1])
7378
.withIcon(PlatformAssets.MINECRAFT_ICON)
7479
.withTypeText(TranslationConstants.DEFAULT_LOCALE)
75-
.withPresentableText(entry.key)
80+
.withPresentableText(key)
7681
}
7782
.toTypedArray()
7883
}

src/main/resources/META-INF/plugin.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,13 @@
855855
level="WARNING"
856856
hasStaticDescription="true"
857857
implementationClass="com.demonwav.mcdev.translations.inspections.WrongTypeInTranslationArgsInspection"/>
858+
<localInspection displayName="Usage of deprecated translation"
859+
groupName="Minecraft"
860+
language="UAST"
861+
enabledByDefault="true"
862+
level="WARNING"
863+
hasStaticDescription="true"
864+
implementationClass="com.demonwav.mcdev.translations.inspections.DeprecatedTranslationInspection"/>
858865
<localInspection displayName="Entity class does not match this entity class"
859866
groupName="Minecraft"
860867
language="JAVA"

0 commit comments

Comments
 (0)