···3232 }
33333434 icon := resolveIcon(fsys)
3535- appName := deriveAppName(desktopName, appimage)
3535+ appName := deriveAppName(desktop, desktopName, appimage)
36363737 // Patch in memory before any filesystem writes so a failure here installs nothing.
3838 if desktop != nil {
···7878 return nil
7979}
80808181-// deriveAppName prefers the AppImage's desktop-file id, which is stable across
8282-// versions, and falls back to the source filename when no desktop entry ships.
8383-func deriveAppName(desktopName string, appimagePath string) string {
8181+// deriveAppName picks the canonical install name, matching GearLever so the
8282+// two tools land at the same path: the desktop entry's Name field (e.g.
8383+// "ES-DE" -> "esde"), then the desktop-file id, then the source filename.
8484+func deriveAppName(desktop *desktopFile, desktopName string, appimagePath string) string {
8585+ if desktop != nil {
8686+ if name, ok := desktop.get("Name", desktopEntrySection); ok && name != "" {
8787+ return sanitizeAppName(name)
8888+ }
8989+ }
8490 if name := strings.TrimSuffix(desktopName, ".desktop"); name != "" {
8585- return name
9191+ return sanitizeAppName(name)
8692 }
8787- return appNameFromPath(appimagePath)
9393+ return sanitizeAppName(appNameFromPath(appimagePath))
9494+}
9595+9696+// sanitizeAppName lowercases s, turns spaces into underscores, and drops any
9797+// character that isn't alphanumeric, underscore, or dot — GearLever's naming
9898+// rule.
9999+func sanitizeAppName(s string) string {
100100+ s = strings.ToLower(s)
101101+ s = strings.ReplaceAll(s, " ", "_")
102102+ var b strings.Builder
103103+ b.Grow(len(s))
104104+ for _, r := range s {
105105+ if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '.' {
106106+ b.WriteRune(r)
107107+ }
108108+ }
109109+ return b.String()
88110}
8911190112func appNameFromPath(path string) string {