Skip to content

Commit 9ffe618

Browse files
authored
Localization string actions delegating to bandlab-localizer (#215)
* Add localization string actions delegating to bandlab-localizer The plugin never edits string resource files directly — every add/remove delegates to the bandlab-localizer CLI, which owns merge, validation, key-sorting and multi-locale output. The actions read the manifest and shell out, so they work even when Gradle sync fails. - "Localizer" submenu (Tools menu, Project View popup, editor popup), shown whenever the project's bandlab-localizer-config.toml resolves: - Update Strings -> `update-strings` (full sync). - Add Strings -> dialog (target-file picker + multi-key paste) -> `update-strings --add-keys --add-keys-to-file`. Targeted; pre-selects the clicked file's `[[file]]` group. - Delete Strings -> dialog -> `update-strings --delete-keys`. - Alt+Enter "Delete String" intention on a `<string>`/`<plurals>` element (any caret position) -> `update-strings --delete-keys` for that one string. - Runs the CLI as a tracked process in a Console tab (using the IDE's captured shell env so the wrapper finds java), then refreshes the manifest's files on completion so open editors reload. - Reads the manifest via the published `com.bandlab.localizer.config:config` (3.2.0-SNAPSHOT for now). Related to #214. * Add editor panel and R.string "Add string" intention; share ops via LocalizerOps - Editor banner (EditorNotificationProvider) on manifest string files, with Update / Add / Delete Strings quick links. - Alt+Enter "Localizer: Add string" intention on an `R.string.X` / `R.plurals.X` Kotlin reference that isn't defined locally yet -> `update-strings --add-keys X`. PSI-only (no Gradle sync). - Extract LocalizerOps so the menu actions, the panel, and the intentions share one CLI-invocation + file-refresh path instead of each building it. * Add caret "Update String" intention via --update-keys Alt+Enter on a `<string>`/`<plurals>` element in a manifest string file re-fetches that one string fresh from Tolgee and overwrites it in place (base + every translation) via `update-strings --update-keys`. Mirrors the Delete String intention; routes through LocalizerOps. * Update Strings scope dialog + Update String on managed R.string refs - "Update Strings" (menu/panel) now opens a dialog: All strings (full sync) or Selected strings (`update-strings --update-keys <keys>`), so the scope is explicit instead of always full-sync. - The "Update String" intention also fires on an `R.string.X` / `R.plurals.X` reference whose key is already defined in a managed base file (re-fetch), mirroring the Add-string intention which fires when the key is not yet local. * Warn on hand-editing localizer-managed string files (banner → warning) * Shorten Update/Delete intention labels (drop "String") * Warn-on-typing for managed string files + drop editor-popup leak - Editor: remove the Localizer submenu from the editor (text) popup — IntelliJ surfaces editor-popup actions in the Alt+Enter list, so it leaked "Update Strings" into the intention bulb. The warning banner + the intentions cover the editor; the submenu stays in Tools menu + Project View. - New: a non-blocking warning when hand-editing a managed string file (TypedHandlerDelegate, once per file per session) nudging toward the Localizer actions. Configurable under Settings > Tools > Localizer (LocalizerSettings + LocalizerSettingsConfigurable), on by default. - Disable buildSearchableOptions (collides with a running IDE; settings stay searchable without it). * Add icons to Localizer Alt+Enter intentions - Implement Iconable on the Update/Delete/Add string intentions - Update ↻ (Actions.Refresh), Delete − (General.Remove), Add + (General.Add) so the bulb-list entries are visually distinguishable * Resolve aliased R.string references in Add/Update intentions - Recognize R.string/R.plurals references reached via an import alias (import com.app.R as appR) or a top-level typealias, not just bare R.string.X / com.app.R.string.X. bandlab-android disambiguates its several per-module R classes with import aliases, so the intentions previously didn't fire on the common audiostretchCommonStringsR.string.X form. - Resolve the receiver head (alias-aware) and check it stands for an R class; keep the textual receiver check as a fallback for unsynced/unindexed code. The key itself need not resolve, so the Add (unresolved-key) case still works. - Code references only; the XML <string>/<plurals> path is untouched. - Tests: import-alias, typealias, and typealias-to-non-R-class cases. * Surface "Add string" as a red-section quick fix; prioritize Update/Delete intentions - Add AddStringUnresolvedQuickFixRegistrar: contributes "Localizer: Add string" to the red error-fix section on an unresolved R.string/R.plurals reference (key not defined yet), via the Kotlin K2 UNRESOLVED_REFERENCE diagnostic (org.jetbrains.kotlin.codeinsight.quickfix.registrar EP). Sits beside Android's "Create string value resource" instead of in the yellow intention list. Handles bare / FQN / import-aliased / typealiased receivers via shared resStringKey. - The always-on AddStringFromReferenceIntention stays as a fallback until the red-section path is confirmed in the IDE (transient duplicate in the unresolved case). - Mark Update/Delete intentions HighPriorityAction so they float to the top of the yellow list (they act on a known-existing string). - Declare K2 plugin-mode support (the diagnostic API is FIR/K2-only) + depend on the Kotlin plugin to register the EP. * Fix managed-file banner label: "Add String" → "Add Strings" Match its Update Strings / Delete Strings siblings (all open multi-key dialogs). * Retire the duplicate always-on "Add string" intention The red-section unresolved-reference quick fix now owns the Add case, so the always-on yellow AddStringFromReferenceIntention is redundant. Move the shared R.string detection helpers (resStringKeyAt / resStringKey / localBaseKeys) to ResStringReference.kt; drop the intention class + its plugin.xml registration. * Sort Update above Delete in the ⌥⏎ popup Both fire on a <string> element in a managed string file; bump Update to PriorityAction.Priority.TOP (Delete stays HIGH) so Update lists first. * Resolve Add-Strings target by R class instead of defaulting to first file - Map R class FQN -> module base path; an unresolved R.string/R.plurals quick fix adds straight to the single mapped target, or prompts when ambiguous/unmapped - Force an explicit target pick in the Add dialog (no silent default), pre-filling the key - Mark the "Add string" quick fix HighPriorityAction so it tops the Alt+Enter list * Replace edit-warning balloon with a blocking, per-branch dialog - First keystroke in a managed file is consumed and opens a blocking dialog (edit on this branch / Update / Add / Delete / cancel) instead of a passive balloon - Remember "edit on this branch" in the project workspace keyed by git branch (.git/HEAD, worktree-aware); unknown branch falls back to ask-once-ever - Drop the now-unused balloon notification group * Keep managed-file banner actions out of the Alt+Enter popup Override EditorNotificationPanel.getIntentionAction() to null so the banner Update/Add/Delete labels stay on the banner but no longer surface as suggested intentions. * Vendor config.jar instead of resolving it from a Maven repository - Bundle config.jar (built from localizer 3.2 code) under libs/, depended on via files() - Declare config's runtime deps explicitly (app.gildor:ktoml-file, kotlinx-serialization-json) since file() deps carry no transitive metadata * Simplify the edit-warning dialog to Edit/Cancel - Drop the per-command buttons; the banner and Alt+Enter intentions already expose Update/Add/Delete, so the dialog just gates hand-editing - No default-highlighted button; the message points to the context actions and editor toolbar * Lead the Alt+Enter list with the Localizer actions - The Add-string quick fix and the Update/Delete intentions use PriorityAction.TOP so they head their groups - Register Update before Delete so they sort in that order * Address Copilot review: offset guards, base-key scan, dispose guard - Guard findElementAt(offset - 1) when the caret is at file start (resStringKeyAt, stringKeyAt) - localBaseKeys: match name= anywhere in the opening tag (not only as the first attribute), and cache the scan (invalidated on PSI change) so a moving caret no longer re-reads every base file - LocalizerRunner: skip the post-process VFS refresh when the project is already disposed * Document the localization string actions in the README Replace the old "Update Localized Strings" section (module-level full re-sync) with a concise Localization Strings section covering the new surfaces: Add on unresolved references, per-key Update/Delete in string files, global Add/Update/Delete, and the managed-file edit reminder. * Removed stale comment * Fix ⌥⏎ order: keep Update above Delete Both intentions were PriorityAction.TOP, which ties and tie-breaks alphabetically — putting Delete above Update. Restore Delete to HIGH so Update (TOP) sorts first while both still lead the intentions group. * Detect the Strings/Plurals member-alias reference form The dominant bandlab-android form is `Strings.key` (typealias `Strings = R.string`, `Plurals = R.plurals`), where the alias lives beside `R` in the same strings module — previously undetected, so "Add string" never appeared. - Recognize a plain-name receiver `Strings`/`Plurals` as a string/plurals reference; map `<pkg>.Strings` → `<pkg>.R` by reading the import (text only, no type resolution — works without a Gradle sync). A matching import is required, so locals named `Strings` do not false-positive. - Add tests for the member-alias forms and a guard that Update outranks Delete in the ⌥⏎ popup. * Gate Strings/Plurals detection to known strings packages A bare-name receiver like `Strings` is ambiguous, so recognizing it as a string reference unconditionally would offer "Add string" on any unrelated class named `Strings`. Gate it on the known strings-module packages — only `<pkg>.Strings`/`<pkg>.Plurals` whose `<pkg>.R` is in the hardcoded map count. - Lift R_CLASS_TO_MODULE to a shared internal constant + isKnownRClass(), reused by both target resolution and reference detection (single source of truth). - Explicit `R.string.X` forms stay ungated (syntactically unambiguous); only the bare-name form is gated. * Drop the Localizer settings; always remind on a new branch Remove the application settings + Tools > Localizer configurable and the warn-on-edit toggle. The per-branch "edit on this branch" memory is the real control; reminding on every fresh branch is intentional — it builds the habit of going through the actions. A toggle can come back if anyone actually asks. * Group Update + Delete at the top of the ⌥⏎ list Both intentions are PriorityAction.TOP so they sit together at the top rather than being split by another TOP-priority action (Android's "Open editor"). IntelliJ only orders by priority tier then alphabetically — no custom ordinal — so the visible order is Delete then Update, accepted as the trade for keeping them adjacent. * Add localization-actions screenshots to the README Replace the stale terminal screenshot with shots of the main areas: add/update from a reference, the managed-file banner + context actions, the global actions, and the edit-warning dialog. Committed under docs/images and referenced by relative path (renders on GitHub; swap to absolute URLs if the in-IDE/Marketplace description needs them). * Adopt localizer 3.2.0 release: ktoml 0.8.1 - Bump app.gildor:ktoml-file 0.8.0 → 0.8.1 (matches localizer 3.2.0) - Vendored libs/config.jar verified byte-identical to the published config 3.2.0 release artifact (libs-release), so no jar change needed * Declare config's runtime deps in the version catalog Move app.gildor:ktoml-file and kotlinx-serialization-json from inline coordinates into gradle/libs.versions.toml, per review.
1 parent 23f09a5 commit 9ffe618

41 files changed

Lines changed: 1823 additions & 225 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,29 @@ The plugin helps invoke these tasks in your IDE's Run tab:
124124

125125
---
126126

127-
## Update Localized Strings
128-
![update strings](https://i.imgur.com/l7BLO2N.png)
127+
## Localization Strings
129128

130-
Update all localized strings for a given module from Tolgee. There are multiple entry points:
131-
1. Right-click on a module that contains string resources.
132-
2. Right-click (or Shift+Cmd+A) on a strings.xml, or strings-plurals.xml file.
129+
Add, update, and delete localization keys without hand-editing string resources — every action shells out to `./localizer/bandlab-localizer`, which owns merging, validation, and multi-locale output. Available whenever `bandlab-localizer-config.toml` resolves (no Gradle sync needed).
133130

134-
The plugin helps invoke `./localizer/bandlab-localizer update-strings` in your IDE's termial tab.
131+
**Add a key that doesn't exist yet.** On an unresolved reference (`Strings.foo` / `R.string.foo`), Alt+Enter → **Localizer: Add string** pulls it from Tolgee into the right module's file.
132+
133+
![Add string from a new reference](docs/images/localization-add-reference.png)
134+
135+
**Update an existing key in place.** On a defined `Strings.foo` / `R.string.foo` reference, Alt+Enter → **Localizer: Update** re-fetches just that key — no full re-sync.
136+
137+
![Update an existing reference](docs/images/localization-update-reference.png)
138+
139+
**Work from the string file.** Open a managed `strings.xml` / `strings-plurals.xml`: a banner links to the actions, and Alt+Enter on a `<string>` offers **Update** / **Delete** for that one key.
140+
141+
![Managed string file — banner and context actions](docs/images/localization-strings-file.png)
142+
143+
**Run an action from anywhere.** Find Action (or the Localizer menu) → **Update / Add / Delete Strings**, choosing the target file.
144+
145+
![Localizer global actions](docs/images/localization-global-actions.png)
146+
147+
**Hand-editing is gated, not blocked.** Typing into a managed file prompts you to use the actions instead; choose *Edit on this branch* to proceed anyway (remembered per Git branch — e.g. a feature branch with un-finalized strings).
148+
149+
![Edit-warning dialog](docs/images/localization-edit-warning.png)
135150

136151
---
137152

build.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ dependencies {
5454
composeUI()
5555
}
5656

57+
// bandlab-localizer config model, vendored as a prebuilt jar (config 3.2.0 release artifact).
58+
// file() deps carry no transitive metadata, so config's runtime deps are declared explicitly.
59+
implementation(files("libs/config.jar"))
60+
implementation(libs.ktoml.file)
61+
implementation(libs.serialization.json)
62+
5763
testImplementation(libs.junit)
5864
// Used in com.intellij.util.lang.UrlClassLoader
5965
// see: https://github.com/JetBrains/intellij-platform-gradle-plugin/issues/2127
@@ -148,6 +154,12 @@ kover {
148154
}
149155

150156
tasks {
157+
// Launches a headless IDE to prebuild the settings search index; collides with a running IDE
158+
// ("only one instance of IDEA") and breaks local builds. Settings stay searchable without it.
159+
buildSearchableOptions {
160+
enabled = false
161+
}
162+
151163
register("patchReadme") {
152164
description = "Updates the IntelliJ IDEA badge in README.md to match targetPlatformVersion in gradle.properties"
153165
group = "documentation"
30.4 KB
Loading
53.9 KB
Loading
18.3 KB
Loading
110 KB
Loading
27 KB
Loading

gradle/libs.versions.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ turbine = "1.2.1"
99
coroutines = "1.10.2"
1010
# https://github.com/ota4j-team/opentest4j/releases
1111
opentest4j = "1.3.0"
12+
# Runtime deps of the vendored bandlab-localizer config.jar (libs/).
13+
# app.gildor ktoml fork — https://github.com/gildor/ktoml/releases
14+
ktoml = "0.8.1"
15+
serialization = "1.8.1"
1216

1317
# plugins
1418
changelog = "2.5.0"
@@ -27,6 +31,8 @@ truth = { group = "com.google.truth", name = "truth", version.ref = "truth" }
2731
opentest4j = { group = "org.opentest4j", name = "opentest4j", version.ref = "opentest4j" }
2832
turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" }
2933
coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
34+
ktoml-file = { group = "app.gildor", name = "ktoml-file", version.ref = "ktoml" }
35+
serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
3036

3137
[plugins]
3238
changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" }

libs/config.jar

43.4 KB
Binary file not shown.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.bandlab.intellij.plugin.localizer
2+
3+
import com.intellij.openapi.components.PersistentStateComponent
4+
import com.intellij.openapi.components.Service
5+
import com.intellij.openapi.components.State
6+
import com.intellij.openapi.components.Storage
7+
import com.intellij.openapi.components.StoragePathMacros
8+
import com.intellij.openapi.components.service
9+
import com.intellij.openapi.project.Project
10+
11+
/**
12+
* Persists the set of git branches on which the developer has explicitly chosen to allow direct
13+
* edits to localizer-managed string files. Stored in the workspace file so the preference is local
14+
* to this developer and not committed to VCS.
15+
*
16+
* When the current branch is in this set, [LocalizerEditWarningTypedHandler] silently permits edits
17+
* without prompting.
18+
*/
19+
@Service(Service.Level.PROJECT)
20+
@State(
21+
name = "BandLabLocalizerEditAllowedBranches",
22+
storages = [Storage(StoragePathMacros.WORKSPACE_FILE)],
23+
)
24+
class EditAllowedBranches : PersistentStateComponent<EditAllowedBranches.State> {
25+
26+
class State {
27+
// MutableList serializes reliably with the IntelliJ XML serializer; Set/LinkedHashSet can
28+
// lose elements on round-trip. The contains/add contract is preserved via list operations.
29+
var branches: MutableList<String> = mutableListOf()
30+
}
31+
32+
private var state = State()
33+
34+
override fun getState(): State = state
35+
36+
override fun loadState(s: State) {
37+
state = s
38+
}
39+
40+
/** True when edits on [branch] should be allowed without a warning dialog. */
41+
fun isAllowed(branch: String): Boolean = branch in state.branches
42+
43+
/** Marks [branch] as allowed; subsequent keystrokes in managed files will pass through silently. */
44+
fun allow(branch: String) {
45+
if (branch !in state.branches) state.branches.add(branch)
46+
}
47+
48+
companion object {
49+
fun getInstance(project: Project): EditAllowedBranches = project.service()
50+
}
51+
}

0 commit comments

Comments
 (0)