Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This plug-in will help you to choose and add the gitmoji, via a button in the Co
* Load the list of gitmoji on startup from the repos of gitmoji (fallback to a local list if error).
* Allow to display the gitmoji in a new column in the commit history
* Translate the gitmoji description in your chinese and french language.
* Custom gitmoji list from url or predefined default [Gitmojis](https://gitmoji.dev/) or [Conventional Gitmojis](https://conventional-gitmoji.web.app/)

See the [gitmoji website](https://gitmoji.dev/) for have the list of Emoji and their signification.
<!-- Plugin description end -->
Expand All @@ -34,6 +35,10 @@ https://plugins.jetbrains.com/plugin/12383-gitmoji/
In IntelliJ, go to preference, then Plugins, and search Gitmoji by Patrice de Saint Steban.
After install, and restart, you will have a button on the commit dialog.

## Customization

See [custom gitmojis](custom_gitmojis.md) documentation.

## Contrib

You can contrib by adding [issues](https://github.com/patou/gitmoji-plus-commit-button/issues/new), or create pull request.
Expand Down
77 changes: 77 additions & 0 deletions custom_gitmojis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Custom Gitmojis

This document explains how to customize which Gitmojis the plugin shows, where the plugin looks for the default data, how to provide your own JSON source, and how localization works.

## Configuring a custom Gitmoji source
1. Open the plugin settings (Settings / Preferences → GitMoji).
2. Set source type to "Custom".
3. Enter the JSON URL for your custom gitmoji list in the JSON URL field.
4. Enter a localization URL template in the Localization URL field. Use `{locale}` as a placeholder that will be replaced with the selected locale (see below).

The plugin expects a JSON object with a `gitmojis` array. Each element must contain `emoji`, `code`, `description`, and `name` fields. Example:

```json
{
"gitmojis": [
{
"emoji": "😄",
"code": ":smile:",
"description": "A happy smile",
"name": "smile"
},
{
"emoji": "✨",
"code": ":sparkles:",
"description": "Add new features",
"name": "sparkles"
}
]
}
```

If the HTTP request fails or returns an invalid response, the plugin will silently fall back to the bundled `gitmojis.json` file.

### Localization
- Localized translations are provided as YAML files mapping gitmoji `name` → localized description.
- Provide YAML translations keyed by `name` (not `code`). The plugin looks up translations by the `name` field from the JSON.
- If the field is left empty, description from the JSON source is used.
- The plugin uses a localization URL template that can be included with the `{locale}` token. Example:

```
https://mydomain.com/gitmojis-{locale}.yaml
```

- When the plugin loads translations it will replace `{locale}` with the selected language code and try to download that YAML file. Example replacements:
- `en_US` → https://.../gitmojis-en_US.yaml
- `fr_FR` → https://.../gitmojis-fr_FR.yaml
- `zh_CN` → https://.../gitmojis-zh_CN.yaml

- Supported config language values:
- `auto` (use system locale if supported, otherwise falls back to `en_US`)
- `en_US`, `zh_CN`, `fr_FR`, `ru_RU`, `pt_BR`

YAML structure example:

```yaml
gitmojis:
smile: "Sourire"
sparkles: "Ajouter de nouvelles fonctionnalités"
```

The plugin will try to download remote YAML translations. If the network fetch or parsing fails, it falls back to bundled local YAML resources named `gitmojis-<locale>.yaml` that is shipped with the plugin or description from the json directly.

## Default data
- The plugin ships a bundled default file at [gitmojis.json](./src/main/resources/gitmojis.json). If an HTTP fetch of the configured JSON URL fails, the plugin falls back to this embedded file.
- Default localization example file can be found at [gitmojis.yaml](./src/main/resources/gitmojis.yaml).
- The default remote URL used by the plugin for Gitmoji source is https://gitmoji.dev/api/gitmojis
- There is also a built-in [Conventional Gitmoji](https://conventional-gitmoji.web.app/) option, which is reduced set of Gitmojis matching the [conventional commit](https://www.conventionalcommits.org) specification.


## Practical example — host custom JSON and YAML on GitHub
1. Create a repository containing `gitmojis.json` in the root and localization files named `gitmojis-fr_FR.yaml`, `gitmojis-zh_CN.yaml`, etc.
2. Use GitHub raw URLs for the two fields in the plugin settings. Example:
- JSON URL: `https://raw.githubusercontent.com/<you>/<repo>/main/gitmojis.json`
- Localization template: `https://raw.githubusercontent.com/<you>/<repo>/main/gitmojis-{locale}.yaml`

## Further reference
Example of conventional config repo for inspiration: https://github.com/glazrtom/conventional-gitmoji-config
142 changes: 139 additions & 3 deletions src/main/kotlin/com/github/patou/gitmoji/GitMojiConfig.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
package com.github.patou.gitmoji

import com.github.patou.gitmoji.source.GitmojiSourceType
import com.github.patou.gitmoji.source.GitmojiSourceTypeMapper
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.ConfigurationException
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.util.Comparing
import java.awt.Color
import java.awt.Cursor
import java.awt.Desktop
import java.awt.FlowLayout
import java.awt.GridLayout
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.net.URI
import javax.swing.JCheckBox
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JTextField

class GitMojiConfig(private val project: Project) : SearchableConfigurable {
private val mainPanel: JPanel
Expand All @@ -33,19 +43,37 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
private var textAfterUnicodeConfig: String = " "
private var languagesConfig:String = "auto"

private val gitmojiSourceField = ComboBox(GitmojiSourceType.OPTIONS)
private val gitmojiJsonUrlField = JTextField(40)
private val localizationUrlField = JTextField(40)
private var gitmojiJsonPanel: JPanel
private var localizationPanel: JPanel
private val sourceTooltipLabel: JLabel = JLabel()
private var gitmojiSourceConfig: GitmojiSourceType = GitmojiSourceType.Gitmoji
private var gitmojiJsonUrlConfig: String = ""
private var localizationUrlConfig: String = ""

override fun isModified(): Boolean =
Configurable.isCheckboxModified(useProjectSettings, useProjectSettingsConfig) ||
Configurable.isCheckboxModified(displayEmoji, displayEmojiConfig == "emoji") ||
Configurable.isCheckboxModified(useUnicode, useUnicodeConfig) ||
isModified(textAfterUnicode, textAfterUnicodeConfig) ||
isModified(languages, languagesConfig) ||
Configurable.isCheckboxModified(insertInCursorPosition, insertInCursorPositionConfig) ||
Configurable.isCheckboxModified(includeGitMojiDescription, includeGitMojiDescriptionConfig)
Configurable.isCheckboxModified(includeGitMojiDescription, includeGitMojiDescriptionConfig) ||
isModified(gitmojiSourceField, gitmojiSourceConfig.id) ||
gitmojiJsonUrlField.text != gitmojiJsonUrlConfig ||
localizationUrlField.text != localizationUrlConfig

private fun isModified(comboBox: ComboBox<String>, value: String): Boolean {
return !Comparing.equal(comboBox.selectedItem, value)
}

private fun <T> isModified(comboBox: ComboBox<OptionItem<T>>, value: T): Boolean {
val selectedItem = comboBox.selectedItem as? OptionItem<*>
return !Comparing.equal(selectedItem?.id, value)
}

override fun getDisplayName(): String = GitmojiBundle.message("projectName")
override fun getId(): String = "com.github.patou.gitmoji.config"

Expand All @@ -66,9 +94,72 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
languageJPanel.add(JLabel(GitmojiBundle.message("config.language")))
languageJPanel.add(languages, null)
mainPanel.add(languageJPanel)

// Gitmoji Source Type panel
val gitmojiSourcePanel = JPanel(FlowLayout(FlowLayout.LEADING))
gitmojiSourcePanel.add(JLabel(GitmojiBundle.message("config.source.type")))
gitmojiSourceField.renderer = OptionItemRenderer()
gitmojiSourcePanel.add(gitmojiSourceField, null)

sourceTooltipLabel.text = gitmojiSourceConfig.tooltipText
sourceTooltipLabel.font = sourceTooltipLabel.font.deriveFont((sourceTooltipLabel.font.size - 2).toFloat())
sourceTooltipLabel.foreground = Color(120, 120, 120)
sourceTooltipLabel.toolTipText = gitmojiSourceConfig.tooltipUrl
sourceTooltipLabel.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
sourceTooltipLabel.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
try {
val url = sourceTooltipLabel.toolTipText
if (!url.isNullOrBlank() && Desktop.isDesktopSupported()) {
Desktop.getDesktop().browse(URI(url))
}
} catch (_: Exception) {
// ignore errors opening link
}
}
})

gitmojiSourcePanel.add(sourceTooltipLabel)
mainPanel.add(gitmojiSourcePanel)

gitmojiSourceField.addItemListener {
val selectedId = getCurrentGitmojiSourceId()
val type = GitmojiSourceTypeMapper.fromId(selectedId, gitmojiJsonUrlField.text.trim(), localizationUrlField.text.trim())
setGitmojiSourceFieldsVisibility(type)
updateSourceTooltip(type)
}

// Gitmoji JSON URL panel
gitmojiJsonPanel = JPanel(FlowLayout(FlowLayout.LEADING))
gitmojiJsonPanel.add(JLabel(GitmojiBundle.message("config.source.jsonUrl")))
gitmojiJsonPanel.add(gitmojiJsonUrlField, null)
mainPanel.add(gitmojiJsonPanel)

// Localization URL panel
localizationPanel = JPanel(FlowLayout(FlowLayout.LEADING))
localizationPanel.add(JLabel(GitmojiBundle.message("config.source.localizationUrl")))
localizationPanel.add(localizationUrlField, null)
mainPanel.add(localizationPanel)
}

override fun apply() {
val gitmojiSourceConfigId = getCurrentGitmojiSourceId()
val jsonUrlCandidate = gitmojiJsonUrlField.text.trim()
val localizationCandidate = localizationUrlField.text.trim()
val gitmojiSource = GitmojiSourceTypeMapper.fromId(gitmojiSourceConfigId, gitmojiJsonUrlConfig, localizationUrlConfig)

if (gitmojiSource is GitmojiSourceType.Custom) {
if (jsonUrlCandidate.isBlank()) {
throw ConfigurationException(GitmojiBundle.message("config.source.error.jsonUrl.empty"))
}
if (!isValidHttpUrl(jsonUrlCandidate)) {
throw ConfigurationException(GitmojiBundle.message("config.source.error.jsonUrl.invalid"))
}
if (localizationCandidate.isNotBlank() && !isValidHttpUrl(localizationCandidate)) {
throw ConfigurationException(GitmojiBundle.message("config.source.error.localizationUrl.invalid"))
}
}

val wasProjectSettings = useProjectSettingsConfig
useProjectSettingsConfig = useProjectSettings.isSelected

Expand All @@ -82,6 +173,9 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
else -> textAfterUnicodeOptions[textAfterUnicode.selectedIndex]
}
languagesConfig = languageOptions[languages.selectedIndex]
gitmojiJsonUrlConfig = jsonUrlCandidate
localizationUrlConfig = localizationCandidate
gitmojiSourceConfig = gitmojiSource

val projectProps = PropertiesComponent.getInstance(project)
val appProps = PropertiesComponent.getInstance()
Expand All @@ -96,13 +190,18 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
propsToSave.setValue(CONFIG_INCLUDE_GITMOJI_DESCRIPTION, includeGitMojiDescriptionConfig)
propsToSave.setValue(CONFIG_AFTER_UNICODE, textAfterUnicodeConfig)
propsToSave.setValue(CONFIG_LANGUAGE, languagesConfig)
propsToSave.setValue(CONFIG_GITMOJI_SOURCE_TYPE, gitmojiSourceConfig.id.value)
propsToSave.setValue(CONFIG_GITMOJI_JSON_URL, gitmojiJsonUrlConfig)
propsToSave.setValue(CONFIG_LOCALIZATION_URL, localizationUrlConfig)

// If we just unchecked, remove project settings
if (wasProjectSettings && !useProjectSettingsConfig) {
clearProjectSettings(projectProps)
}

GitmojiLocale.loadTranslations()
GitmojiLocale.loadTranslations(project)
Gitmojis.gitmojis.clear()
Gitmojis.ensureGitmojisLoaded(project)
}

private fun clearProjectSettings(props: PropertiesComponent) {
Expand All @@ -113,6 +212,9 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
props.unsetValue(CONFIG_INCLUDE_GITMOJI_DESCRIPTION)
props.unsetValue(CONFIG_AFTER_UNICODE)
props.unsetValue(CONFIG_LANGUAGE)
props.unsetValue(CONFIG_GITMOJI_SOURCE_TYPE)
props.unsetValue(CONFIG_GITMOJI_JSON_URL)
props.unsetValue(CONFIG_LOCALIZATION_URL)
} catch (_: Exception) {
// Ignore errors during cleanup
}
Expand All @@ -135,6 +237,10 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
includeGitMojiDescriptionConfig = props.getBoolean(CONFIG_INCLUDE_GITMOJI_DESCRIPTION, false)
textAfterUnicodeConfig = props.getValue(CONFIG_AFTER_UNICODE, " ")
languagesConfig = props.getValue(CONFIG_LANGUAGE, "auto")
gitmojiJsonUrlConfig = props.getValue(CONFIG_GITMOJI_JSON_URL, "")
localizationUrlConfig = props.getValue(CONFIG_LOCALIZATION_URL, "")
val gitmojiSourceConfigId = (props.getValue(CONFIG_GITMOJI_SOURCE_TYPE, GitmojiSourceType.Gitmoji.id.value)).let(GitmojiSourceType::Id)
gitmojiSourceConfig = GitmojiSourceTypeMapper.fromId(gitmojiSourceConfigId, gitmojiJsonUrlConfig, localizationUrlConfig)

displayEmoji.isSelected = displayEmojiConfig == "emoji"
useUnicode.isSelected = useUnicodeConfig
Expand All @@ -144,7 +250,37 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
-1 -> if (textAfterUnicodeConfig == " ") 1 else 0
else -> textAfterUnicodeOptions.indexOf(textAfterUnicodeConfig)
}
languages.selectedIndex = languageOptions.indexOf(languagesConfig)
languages.selectedIndex = languageOptions.indexOf(languagesConfig).coerceAtLeast(0)
gitmojiSourceField.selectedIndex = GitmojiSourceType.OPTIONS.indexOfFirst { it.id == gitmojiSourceConfig.id }.coerceAtLeast(0)
gitmojiJsonUrlField.text = gitmojiJsonUrlConfig
localizationUrlField.text = localizationUrlConfig
setGitmojiSourceFieldsVisibility(gitmojiSourceConfig)
updateSourceTooltip(gitmojiSourceConfig)
}

private fun setGitmojiSourceFieldsVisibility(type: GitmojiSourceType) {
val gitmojiSourceFieldsVisibility = type.id == GitmojiSourceType.Custom.ID
gitmojiJsonPanel.isVisible = gitmojiSourceFieldsVisibility
localizationPanel.isVisible = gitmojiSourceFieldsVisibility
}

private fun updateSourceTooltip(type: GitmojiSourceType) {
sourceTooltipLabel.text = type.tooltipText
sourceTooltipLabel.toolTipText = type.tooltipUrl
}

private fun getCurrentGitmojiSourceId(): GitmojiSourceType.Id {
return (gitmojiSourceField.selectedItem as OptionItem<*>).id as GitmojiSourceType.Id
}

private fun isValidHttpUrl(url: String): Boolean {
return try {
val uri = URI(url.replace("{locale}", "en_US"))
val scheme = uri.scheme?.lowercase()
(scheme == "http" || scheme == "https") && !uri.host.isNullOrBlank()
} catch (_: Exception) {
false
}
}

override fun createComponent(): JComponent = mainPanel
Expand Down
4 changes: 4 additions & 0 deletions src/main/kotlin/com/github/patou/gitmoji/GitmojiData.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.patou.gitmoji

import com.github.patou.gitmoji.source.GitmojiSourceType

Check warning on line 3 in src/main/kotlin/com/github/patou/gitmoji/GitmojiData.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Unused import directive

Unused import directive
import com.intellij.openapi.util.IconLoader
import javax.swing.Icon

Expand All @@ -10,6 +11,9 @@
const val CONFIG_INSERT_IN_CURSOR_POSITION: String = "com.github.patou.gitmoji.insert-in-cursor-position"
const val CONFIG_INCLUDE_GITMOJI_DESCRIPTION: String = "com.github.patou.gitmoji.include-gitmoji-description"
const val CONFIG_USE_PROJECT_SETTINGS: String = "com.github.patou.gitmoji.use-project-settings"
const val CONFIG_GITMOJI_SOURCE_TYPE: String = "com.github.patou.gitmoji.gitmoji-source-type"
const val CONFIG_GITMOJI_JSON_URL: String = "com.github.patou.gitmoji.gitmoji-json-url"
const val CONFIG_LOCALIZATION_URL: String = "com.github.patou.gitmoji.localization-url"

data class GitmojiData(val code: String, val emoji: String, val description: String, val name: String) {
private lateinit var _icon: Icon
Expand Down
14 changes: 9 additions & 5 deletions src/main/kotlin/com/github/patou/gitmoji/GitmojiLocale.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.github.patou.gitmoji

import com.github.patou.gitmoji.source.GitmojiSourceTypeProvider
import com.intellij.ide.util.PropertiesComponent

Check warning on line 4 in src/main/kotlin/com/github/patou/gitmoji/GitmojiLocale.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Unused import directive

Unused import directive
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import okhttp3.*
import org.yaml.snakeyaml.Yaml
import java.io.IOException
Expand All @@ -22,10 +24,11 @@
return translations[name] ?: return description
}

fun loadTranslations() {
fun loadTranslations(project: Project) {
translations.clear()
val instance = PropertiesComponent.getInstance()
var language = instance.getValue(CONFIG_LANGUAGE) ?: "auto"
val sourceType = GitmojiSourceTypeProvider.provide(project)
val props = ConfigUtil.propsFor(project)
var language = props.getValue(CONFIG_LANGUAGE) ?: "auto"
if (language == "auto") {
val defaultLanguage = Locale.getDefault().toString()
if (LANGUAGE_CONFIG_LIST.contains(defaultLanguage)) {
Expand All @@ -35,13 +38,14 @@
language = "en_US"
}
}
if (language == "en_US") {
val requestUrl = sourceType.getLocalizedUrl(language)
if (language == "en_US" || requestUrl.isBlank()) {
// no need to load english translations, as they are the default
return
}
val client = OkHttpClient().newBuilder().addInterceptor(SafeGuardInterceptor()).build()
val request: Request = Request.Builder()
.url("https://raw.githubusercontent.com/patou/gitmoji-plus-commit-button/master/src/main/resources/gitmojis-$language.yaml")
.url(requestUrl)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class GitmojiStartupActivity : ProjectActivity {

override suspend fun execute(project: Project) {
offerMigrationIfNeeded(project)
Gitmojis.ensureGitmojisLoaded()
Gitmojis.ensureGitmojisLoaded(project)
}

private fun offerMigrationIfNeeded(project: Project) {
Expand Down
Loading
Loading