diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 2e1d439e..cecfc565 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -21,7 +21,7 @@ A81B98182BDC854F005FD78C /* AboutConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81B98172BDC854F005FD78C /* AboutConfiguration.swift */; }; A81D8D0A2C068B8700188E12 /* LuminarePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81D8D092C068B8700188E12 /* LuminarePreviewView.swift */; }; A81D8D0C2C06950000188E12 /* LuminareManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81D8D0B2C06950000188E12 /* LuminareManager.swift */; }; - A82521EE29E235AC00139654 /* PermissionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82521ED29E235AC00139654 /* PermissionsManager.swift */; }; + A82521EE29E235AC00139654 /* AccessibilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82521ED29E235AC00139654 /* AccessibilityManager.swift */; }; A82740982AB00FCE00B9BDC5 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82740972AB00FCE00B9BDC5 /* Color+Extensions.swift */; }; A827409A2AB0208500B9BDC5 /* TriggerKeycorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82740992AB0208500B9BDC5 /* TriggerKeycorder.swift */; }; A82B1AEE2BD352A100E2F3F9 /* AccentColorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82B1AED2BD352A100E2F3F9 /* AccentColorConfiguration.swift */; }; @@ -110,7 +110,7 @@ A81B98172BDC854F005FD78C /* AboutConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutConfiguration.swift; sourceTree = ""; }; A81D8D092C068B8700188E12 /* LuminarePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LuminarePreviewView.swift; sourceTree = ""; }; A81D8D0B2C06950000188E12 /* LuminareManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LuminareManager.swift; sourceTree = ""; }; - A82521ED29E235AC00139654 /* PermissionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsManager.swift; sourceTree = ""; }; + A82521ED29E235AC00139654 /* AccessibilityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityManager.swift; sourceTree = ""; }; A82740972AB00FCE00B9BDC5 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = ""; }; A82740992AB0208500B9BDC5 /* TriggerKeycorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggerKeycorder.swift; sourceTree = ""; }; A82B1AED2BD352A100E2F3F9 /* AccentColorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccentColorConfiguration.swift; sourceTree = ""; }; @@ -265,7 +265,7 @@ A87DDD142B50A6A400A32C76 /* ScreenManager.swift */, A8A2ABE62A3FB0370067B5A9 /* KeybindMonitor.swift */, A864F4672AA660CD00579738 /* WindowDragManager.swift */, - A82521ED29E235AC00139654 /* PermissionsManager.swift */, + A82521ED29E235AC00139654 /* AccessibilityManager.swift */, A8EF1F08299C87DF00633440 /* IconManager.swift */, A869C1A02B38C6E600AD1A84 /* SystemWindowManager.swift */, ); @@ -635,7 +635,7 @@ 4C6B93E82C1DCF6E00AFF832 /* Updater.swift in Sources */, A8330ACF2A3AC1E900673C8D /* View+Extensions.swift in Sources */, A8427E662C02594E00F20759 /* ExcludedAppsConfiguration.swift in Sources */, - A82521EE29E235AC00139654 /* PermissionsManager.swift in Sources */, + A82521EE29E235AC00139654 /* AccessibilityManager.swift in Sources */, A8E59C4A297F98670064D4BA /* RadialMenuController.swift in Sources */, 0AFE802E2BB98E81009CF06F /* WindowDirection+LocalizedString.swift in Sources */, A8A583B82BE5A117005F4CB2 /* CycleActionConfigurationView.swift in Sources */, diff --git a/Loop/Extensions/Color+Extensions.swift b/Loop/Extensions/Color+Extensions.swift index 947654fd..1fd5b1b7 100644 --- a/Loop/Extensions/Color+Extensions.swift +++ b/Loop/Extensions/Color+Extensions.swift @@ -62,6 +62,18 @@ extension NSColor { return 0.299 * rgbColor.redComponent + 0.587 * rgbColor.greenComponent + 0.114 * rgbColor.blueComponent } + /// Returns the saturation component of the color. + /// Higher values indicate more vibrant colors. + var saturationComponent: CGFloat { + guard let hsbColor = usingColorSpace(.deviceRGB) else { return 0 } + var hue: CGFloat = 0 + var saturation: CGFloat = 0 + var brightness: CGFloat = 0 + var alpha: CGFloat = 0 + hsbColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + return saturation + } + /// Determines if two colors are similar based on a threshold. /// - Parameters: /// - color: The color to compare with the receiver. @@ -71,10 +83,23 @@ extension NSColor { // Convert both colors to the RGB color space for comparison. guard let color1 = usingColorSpace(.deviceRGB), let color2 = color.usingColorSpace(.deviceRGB) else { return false } - // Compare the red, green, and blue components of both colors. - return abs(color1.redComponent - color2.redComponent) < threshold && - abs(color1.greenComponent - color2.greenComponent) < threshold && - abs(color1.blueComponent - color2.blueComponent) < threshold + + // Calculate difference in HSB space for better perceptual comparison + var hue1: CGFloat = 0, sat1: CGFloat = 0, bri1: CGFloat = 0, alpha1: CGFloat = 0 + var hue2: CGFloat = 0, sat2: CGFloat = 0, bri2: CGFloat = 0, alpha2: CGFloat = 0 + + color1.getHue(&hue1, saturation: &sat1, brightness: &bri1, alpha: &alpha1) + color2.getHue(&hue2, saturation: &sat2, brightness: &bri2, alpha: &alpha2) + + // Hue is circular, so we need to account for colors like red (0) and purplish-red (0.95) + let hueDiff = min(abs(hue1 - hue2), 1 - abs(hue1 - hue2)) + + // For low saturation colors, hue matters less + let hueThreshold = threshold * (1 + (1 - min(sat1, sat2))) + + return hueDiff < hueThreshold && + abs(sat1 - sat2) < threshold * 1.5 && + abs(bri1 - bri2) < threshold * 1.5 } /// Quantizes the color to a limited set of values. diff --git a/Loop/Extensions/Defaults+Extensions.swift b/Loop/Extensions/Defaults+Extensions.swift index 5dd23cf0..0fa8022d 100644 --- a/Loop/Extensions/Defaults+Extensions.swift +++ b/Loop/Extensions/Defaults+Extensions.swift @@ -28,6 +28,10 @@ extension Defaults.Keys { static let radialMenuVisibility = Key("radialMenuVisibility", default: true, iCloud: true) static let radialMenuCornerRadius = Key("radialMenuCornerRadius", default: 50, iCloud: true) static let radialMenuThickness = Key("radialMenuThickness", default: 22, iCloud: true) + /// Lock radial menu to the center of the screen + /// Adjust with `defaults write com.MrKai77.Loop lockRadialMenuToCenter -bool true` + /// Reset with `defaults delete com.MrKai77.Loop lockRadialMenuToCenter` + static let lockRadialMenuToCenter = Key("lockRadialMenuToCenter", default: false, iCloud: true) // Preview static let previewVisibility = Key("previewVisibility", default: true, iCloud: true) diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index e1a270ce..68ce5483 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -6,7 +6,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%@" } }, @@ -24,7 +24,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%@" } }, @@ -54,13 +54,13 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%@" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "%@" } }, @@ -101,7 +101,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "%@ está actualmente en uso como la tecla de activación" } }, @@ -237,7 +237,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%lld" } }, @@ -255,7 +255,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%lld" } }, @@ -279,19 +279,19 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%lld" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%lld" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "%lld" } }, @@ -640,7 +640,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Wallpaper" } }, @@ -794,7 +794,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Por favor, concede permiso para que Loop pueda cambiar el tamaño de las ventanas" } }, @@ -1311,7 +1311,6 @@ } }, "Apply padding" : { - "comment" : "", "extractionState" : "manual", "localizations" : { "ar" : { @@ -1340,8 +1339,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Apply padding" + "state" : "translated", + "value" : "Ajouter des marges" } }, "it" : { @@ -1774,7 +1773,14 @@ } }, "Brisk" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "활발한" + } + } + } }, "Cannot be enabled when the preview is disabled." : { "extractionState" : "manual", @@ -1799,7 +1805,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "No se puede activar cuando la previsualización esté desactivada" } }, @@ -1953,7 +1959,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Comprobar actualizaciones..." } }, @@ -1983,14 +1989,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Verificar atualizações..." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Проверить наличие обновлений..." + "state" : "translated", + "value" : "Проверить наличие обновлений…" } }, "zh-Hans" : { @@ -2107,7 +2113,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Color" } }, @@ -2184,7 +2190,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Configurar margen..." } }, @@ -2214,14 +2220,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Configurar espaçamento..." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Настроить отступы..." + "state" : "translated", + "value" : "Настроить отступы…" } }, "zh-Hans" : { @@ -2439,7 +2445,7 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Credits" } }, @@ -2492,7 +2498,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Cursor" } }, @@ -2522,7 +2528,7 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Cursor" } }, @@ -2800,7 +2806,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Personalizar la acción de esta combinación" } }, @@ -2877,7 +2883,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Personalizar el marco de esta combinación" } }, @@ -2954,7 +2960,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Personalizar la rotación de esta combinación" } }, @@ -2984,14 +2990,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Personalize o que esta combinação de teclas alterna" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Изменить цикл горячей клавиши" + "state" : "translated", + "value" : "Изменить цикл горячей клавиши." } }, "zh-Hans" : { @@ -3138,7 +3144,7 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Design" } }, @@ -3552,7 +3558,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "تبرع..." } }, @@ -3570,7 +3576,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Donar..." } }, @@ -3600,14 +3606,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Doar..." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Пожертвовать..." + "state" : "translated", + "value" : "Пожертвовать…" } }, "zh-Hans" : { @@ -3724,7 +3730,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Asegúrate de que el archivo seleccionado está en el formato adecuado" } }, @@ -3961,8 +3967,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Export" + "state" : "translated", + "value" : "Exporter" } }, "it" : { @@ -4038,8 +4044,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You can't export something that doesn't exist!" + "state" : "translated", + "value" : "Impossible d’exporter quelque chose qui n’existe pas !" } }, "it" : { @@ -4115,8 +4121,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "No Keybinds Have Been Set" + "state" : "translated", + "value" : "Aucun raccourci clavier n'a été défini" } }, "it" : { @@ -4192,8 +4198,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Export keybinds" + "state" : "translated", + "value" : "Exporter les raccourcis" } }, "it" : { @@ -4269,8 +4275,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "External bar" + "state" : "translated", + "value" : "Bar externe" } }, "it" : { @@ -4346,8 +4352,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Fast" + "state" : "translated", + "value" : "Rapide" } }, "it" : { @@ -4395,7 +4401,14 @@ } }, "Fluid" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "체액" + } + } + } }, "Focus window on resize" : { "extractionState" : "manual", @@ -4426,8 +4439,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Focus window on resize" + "state" : "translated", + "value" : "Donner le focus à la fenêtre lors du redimensionnement" } }, "it" : { @@ -4497,14 +4510,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "General" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "General" + "state" : "translated", + "value" : "Général" } }, "it" : { @@ -4556,7 +4569,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Go Back" } }, @@ -4574,14 +4587,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Go Back" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Go Back" + "state" : "translated", + "value" : "Réinitialiser" } }, "it" : { @@ -4592,8 +4605,8 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Go Back" + "state" : "translated", + "value" : "뒤로 가기" } }, "nl-BE" : { @@ -4604,14 +4617,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Go Back" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Go Back" + "state" : "translated", + "value" : "Назад" } }, "zh-Hans" : { @@ -4657,8 +4670,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Gradient" + "state" : "translated", + "value" : "Dégradé" } }, "it" : { @@ -4734,8 +4747,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Green" + "state" : "translated", + "value" : "Vert" } }, "it" : { @@ -4787,7 +4800,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Grow" } }, @@ -4805,14 +4818,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Grow" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Grow" + "state" : "translated", + "value" : "Agrandir" } }, "it" : { @@ -4835,14 +4848,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Grow" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Grow" + "state" : "translated", + "value" : "Увеличить" } }, "zh-Hans" : { @@ -4888,8 +4901,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Halves" + "state" : "translated", + "value" : "Moitiés" } }, "it" : { @@ -4965,8 +4978,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Haptic feedback" + "state" : "translated", + "value" : "Retour haptique" } }, "it" : { @@ -5042,8 +5055,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Height" + "state" : "translated", + "value" : "Hauteur" } }, "it" : { @@ -5119,8 +5132,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Hide menu bar icon" + "state" : "translated", + "value" : "Masquer l’icône de la barre de menu" } }, "it" : { @@ -5196,8 +5209,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Hide until direction is chosen" + "state" : "translated", + "value" : "Masquer jusqu’à ce qu'une direction soit choisie" } }, "it" : { @@ -5273,8 +5286,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Horizontal Thirds" + "state" : "translated", + "value" : "Tiers horizontal" } }, "it" : { @@ -5350,8 +5363,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Icon" + "state" : "translated", + "value" : "Icône" } }, "it" : { @@ -5427,8 +5440,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Black" + "state" : "translated", + "value" : "Noir" } }, "it" : { @@ -5504,8 +5517,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Black Hole" + "state" : "translated", + "value" : "Trou noir" } }, "it" : { @@ -5581,7 +5594,7 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Classic" } }, @@ -5630,11 +5643,11 @@ } }, "Icon Name: Daylight" : { - "extractionState" : "manual", + "extractionState" : "extracted_with_value", "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Daylight" } }, @@ -5646,19 +5659,19 @@ }, "en" : { "stringUnit" : { - "state" : "translated", + "state" : "new", "value" : "Daylight" } }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Daylight" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Daylight" } }, @@ -5670,25 +5683,25 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Daylight" + "state" : "translated", + "value" : "주간" } }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Daylight" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Daylight" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Daylight" } }, @@ -5735,8 +5748,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Developer" + "state" : "translated", + "value" : "Developpeur" } }, "it" : { @@ -5806,13 +5819,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Holo" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Holo" } }, @@ -5830,7 +5843,7 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Holo" } }, @@ -5889,7 +5902,7 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Keycap" } }, @@ -5919,7 +5932,7 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Keycap" } }, @@ -5960,13 +5973,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Loop Master" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Loop Master" } }, @@ -6037,13 +6050,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Meta Loop" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Meta Loop" } }, @@ -6061,19 +6074,19 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Meta Loop" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Meta Loop" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Meta Loop" } }, @@ -6114,14 +6127,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Neon" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Neon" + "state" : "translated", + "value" : "Néon" } }, "it" : { @@ -6138,13 +6151,13 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Neon" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Neon" } }, @@ -6191,13 +6204,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Rosé Pine" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Rosé Pine" } }, @@ -6215,19 +6228,19 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Rosé Pine" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Rosé Pine" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Rosé Pine" } }, @@ -6268,13 +6281,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Simon" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Simon" } }, @@ -6298,13 +6311,13 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Simon" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Simon" } }, @@ -6345,14 +6358,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Summer" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Summer" + "state" : "translated", + "value" : "Été" } }, "it" : { @@ -6422,13 +6435,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Synthwave Sunset" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Synthwave Sunset" } }, @@ -6458,7 +6471,7 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Synthwave Sunset" } }, @@ -6499,14 +6512,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "White" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "White" + "state" : "translated", + "value" : "Blanc" } }, "it" : { @@ -6576,14 +6589,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Serás notificado cuando hayas desbloqueado un nuevo icono" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You will now be notified when you unlock a new icon." + "state" : "translated", + "value" : "Vous serez désormais averti lorsque vous débloquez une nouvelle icône." } }, "it" : { @@ -6612,8 +6625,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Вам придет уведомление когда вы откроете новую иконку." + "state" : "translated", + "value" : "Вам придет уведомление когда вы откроете новую иконку." } }, "zh-Hans" : { @@ -6659,8 +6672,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You've unlocked a new icon: %@!" + "state" : "translated", + "value" : "Vous avez débloqué une nouvelle icône : %@ !" } }, "it" : { @@ -6736,8 +6749,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You've already looped 25 times! As a reward, here's new icon: %@. Continue to loop more to unlock new icons!" + "state" : "translated", + "value" : "Vous avez utilisé Loop 25 fois ! En récompense, voici une nouvelle icône : %@. Continuez à boucler pour en débloquer d’autres !" } }, "it" : { @@ -6760,7 +6773,7 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Você ja fez 25 loops! Aqui está um novo ícone como prêmio: %@. Continue dando loops para desbloquear novos ícones." } }, @@ -6813,8 +6826,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "5000 loops conquered! The universe has witnessed the birth of a Loop master! Enjoy your well-deserved reward: a brand-new icon!" + "state" : "translated", + "value" : "5000 Loops accomplies ! L’univers a été témoin de la naissance d’un maître de Loop ! Profitez de votre récompense bien méritée : une toute nouvelle icône !" } }, "it" : { @@ -6890,8 +6903,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Ignore fullscreen windows" + "state" : "translated", + "value" : "Ignorer les fenêtres en plein écran" } }, "it" : { @@ -6967,8 +6980,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Import" + "state" : "translated", + "value" : "Importer" } }, "it" : { @@ -7044,8 +7057,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Select Rectangle config file" + "state" : "translated", + "value" : "Selectionner le fichier de configuration de Rectangle" } }, "it" : { @@ -7121,8 +7134,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Import Keybinds" + "state" : "translated", + "value" : "Importer des raccourcis" } }, "it" : { @@ -7198,8 +7211,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Import keybinds from Rectangle" + "state" : "translated", + "value" : "Importer des raccourcis de Rectangle" } }, "it" : { @@ -7275,8 +7288,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Cancel" + "state" : "translated", + "value" : "Annuler" } }, "it" : { @@ -7352,8 +7365,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Erase" + "state" : "translated", + "value" : "Effacer" } }, "it" : { @@ -7429,8 +7442,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Merge" + "state" : "translated", + "value" : "Fusionner" } }, "it" : { @@ -7506,8 +7519,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Include development versions" + "state" : "translated", + "value" : "Inclure les versions de développement" } }, "it" : { @@ -7583,8 +7596,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Initial Size" + "state" : "translated", + "value" : "Taille initiale" } }, "it" : { @@ -7660,8 +7673,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Install" + "state" : "translated", + "value" : "Installer" } }, "it" : { @@ -7737,8 +7750,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Instant" + "state" : "translated", + "value" : "Instantané" } }, "it" : { @@ -8348,7 +8361,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "icono bloqueado" } }, @@ -8461,8 +8474,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "У вас этого ещё нет?" + "state" : "translated", + "value" : "У вас этого ещё нет!" } }, "zh-Hans" : { @@ -8579,7 +8592,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "La paciencia es una virtud, y tu llave a este icono" } }, @@ -8733,7 +8746,7 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Sigue haciendo Loops y este icono será tuyo pronto" } }, @@ -9740,8 +9753,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Every Loop brings you closer to the treasure that awaits." + "state" : "translated", + "value" : "Chaque Loop vous rapproche du trésor qui vous attend." } }, "it" : { @@ -9817,8 +9830,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "With each Loop, the lock on this icon weakens." + "state" : "translated", + "value" : "À chaque Loop, le verrou sur cette icône s’affaiblit." } }, "it" : { @@ -9894,8 +9907,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Loop after Loop, your dedication carves the key to success." + "state" : "translated", + "value" : "Loop après Loop, votre détermination façonne la clé du succès." } }, "it" : { @@ -9971,8 +9984,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "The icons are not just unlocked; they're earned, Loop by Loop." + "state" : "translated", + "value" : "Les icônes ne sont pas débloquées par défaut ; elles sont gagnées Loop après Loop." } }, "it" : { @@ -9995,13 +10008,13 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Os ícones não são apenas desbloqueados; são conquistados, Loop por Loop. " } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Иконки не открываются сами по себе, их надо заслужить!" } }, @@ -10048,8 +10061,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "As the Loops accumulate, so too will your collection of icons." + "state" : "translated", + "value" : "À mesure que les Loops s’accumulent, votre collection d’icônes grandira elle aussi." } }, "it" : { @@ -10125,8 +10138,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Think of each Loop as a riddle, solving the mystery of the locked icon." + "state" : "translated", + "value" : "Chaque Loop est une énigme à résoudre, pour percer le mystère des icônes verrouillées." } }, "it" : { @@ -10202,8 +10215,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Your persistence in Looping is the master key to all icons." + "state" : "translated", + "value" : "Votre persévérance a Looper est la passe-partout de toutes les icônes." } }, "it" : { @@ -10279,8 +10292,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Loop around the obstacles; your reward is just beyond them." + "state" : "translated", + "value" : "Loop autour des obstacles ; votre récompense est juste derrière." } }, "it" : { @@ -10356,8 +10369,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Each Loop you complete plants the seeds for icons to grow." + "state" : "translated", + "value" : "Chaque Loop que vous accomplissez sème les graines d’icônes à venir." } }, "it" : { @@ -10433,8 +10446,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Like the moon's phases, your icons will reveal themselves in cycles of Loops." + "state" : "translated", + "value" : "À l’image des phases de la lune, vos icônes se dévoileront au fil des cycles de Loops." } }, "it" : { @@ -10510,8 +10523,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "The icons await, hidden behind the veil of Loops yet to be made." + "state" : "translated", + "value" : "Les icônes patientent, cachées derrière le voile des Loops à venir." } }, "it" : { @@ -10566,37 +10579,37 @@ "plural" : { "few" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "بقي %lld تكرار" } }, "many" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "بقي %lld تكرار" } }, "one" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "بقي تكرار واحد" } }, "other" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%lld Loops left" } }, "two" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "بقي تكراران" } }, "zero" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "بقي %lld تكرار" } } @@ -10662,14 +10675,14 @@ "plural" : { "one" : { "stringUnit" : { - "state" : "needs_review", - "value" : "%lld Loop left" + "state" : "translated", + "value" : "%lld Loop restant" } }, "other" : { "stringUnit" : { - "state" : "needs_review", - "value" : "%lld Loops left" + "state" : "translated", + "value" : "%lld Loops restant" } } } @@ -10802,7 +10815,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "macOS's \"Tile by dragging windows to screen edges\" feature is currently\nenabled, which will conflict with Loop's window snapping functionality." } }, @@ -10820,14 +10833,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "macOS's \"Tile by dragging windows to screen edges\" feature is currently\nenabled, which will conflict with Loop's window snapping functionality." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "macOS's \"Tile by dragging windows to screen edges\" feature is currently\nenabled, which will conflict with Loop's window snapping functionality." + "state" : "translated", + "value" : "La fonctionnalité macOS \"Faire glisser les fenêtres vers les bords de l'écran pour créer une mosaïque\" est actuellement activée, ce qui entre en conflit avec le magnétisme des fenêtres de Loop." } }, "it" : { @@ -10838,26 +10851,26 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "macOS's \"Tile by dragging windows to screen edges\" feature is currently\nenabled, which will conflict with Loop's window snapping functionality." + "state" : "translated", + "value" : "macOS의 \"창을 화면 가장자리로 드래그하여 타일 배치\" 기능이 현재 활성화되어 있으며, 이는 Loop의 창 스냅 기능과 충돌할 수 있습니다." } }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "macOS's \"Tile by dragging windows to screen edges\" feature is currently\nenabled, which will conflict with Loop's window snapping functionality." } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "macOS's \"Tile by dragging windows to screen edges\" feature is currently\nenabled, which will conflict with Loop's window snapping functionality." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "macOS's \"Tile by dragging windows to screen edges\" feature is currently\nenabled, which will conflict with Loop's window snapping functionality." + "state" : "translated", + "value" : "Настройка плиточного расположения окон macOS включена, отключите привязку окон в настройках Loop." } }, "zh-Hans" : { @@ -10897,13 +10910,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "%" } }, @@ -10921,19 +10934,19 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "%" } }, @@ -10974,13 +10987,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "px" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "px" } }, @@ -10998,19 +11011,19 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "px" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "px" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "px" } }, @@ -11051,13 +11064,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "s" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "s" } }, @@ -11075,13 +11088,13 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "s" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "s" } }, @@ -11134,8 +11147,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Middle-click to trigger" + "state" : "translated", + "value" : "Clic central pour activer" } }, "it" : { @@ -11211,8 +11224,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "More" + "state" : "translated", + "value" : "Plus" } }, "it" : { @@ -11264,7 +11277,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Move" } }, @@ -11282,14 +11295,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Move" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Move" + "state" : "translated", + "value" : "Déplacer" } }, "it" : { @@ -11312,14 +11325,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Move" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Move" + "state" : "translated", + "value" : "Двигать" } }, "zh-Hans" : { @@ -11365,8 +11378,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Move cursor with window" + "state" : "translated", + "value" : "Déplacer le curseur avec la fenêtre" } }, "it" : { @@ -11414,7 +11427,6 @@ } }, "Move window to cursor's screen" : { - "comment" : "", "extractionState" : "manual", "localizations" : { "ar" : { @@ -11443,8 +11455,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Move window to cursor's screen" + "state" : "translated", + "value" : "Déplacer la fenêtre sur l’écran où se trouve le curseur" } }, "it" : { @@ -11520,8 +11532,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "No excluded applications" + "state" : "translated", + "value" : "Pas d'applications ignorées" } }, "it" : { @@ -11597,8 +11609,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "No keybinds" + "state" : "translated", + "value" : "Pas de raccourcis" } }, "it" : { @@ -11668,14 +11680,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "¡Comunícate! ...en la versión actual, es la última." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Engage! …in the current version, it's the latest." + "state" : "translated", + "value" : "Engagez ! … avec la version actuelle. C’est la plus récente." } }, "it" : { @@ -11745,14 +11757,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Esta aplicación está más actualizada que las entradas de mi diario." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "This app is more up to date than my diary entries!" + "state" : "translated", + "value" : "Cette appli est mieux tenue à jour que mon journal intime !" } }, "it" : { @@ -11828,8 +11840,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You're in the clear, no updates in the atmosphere!" + "state" : "translated", + "value" : "Ciel dégagé : aucune mise à jour à l’horizon !" } }, "it" : { @@ -11905,8 +11917,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "The odds are ever in your favor, no updates today!" + "state" : "translated", + "value" : "Que la chance soit toujours avec vous… pas de mise à jour aujourd’hui !" } }, "it" : { @@ -11982,8 +11994,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Our app is on a digital diet. No new bytes allowed." + "state" : "translated", + "value" : "L’appli est au régime… numérique. Pas un octet de plus." } }, "it" : { @@ -12059,8 +12071,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "New version? Sorry, we're too attached to this one." + "state" : "translated", + "value" : "Une nouvelle version ? Non merci, on adore celle-là." } }, "it" : { @@ -12136,8 +12148,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Your Loop is Loopier than ever, no updates found!" + "state" : "translated", + "value" : "-" } }, "it" : { @@ -12213,8 +12225,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "I'm giving it all she's got, Captain! No updates!" + "state" : "translated", + "value" : "-" } }, "it" : { @@ -12243,8 +12255,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Люк, я твоё... Обновление" + "state" : "translated", + "value" : "Люк, я твоё... Обновление!" } }, "zh-Hans" : { @@ -12290,8 +12302,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "In a galaxy far, far away… still no updates!" + "state" : "translated", + "value" : "Dans une galaxie lointaine, très lointaine… toujours pas de mise à jour !" } }, "it" : { @@ -12367,8 +12379,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You've got the precious, no updates needed!" + "state" : "translated", + "value" : "Vous possédez le précieux. Nul besoin de mise à jour !" } }, "it" : { @@ -12444,8 +12456,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Riding at warp speed, no updates in sight!" + "state" : "translated", + "value" : "À vitesse lumière… aucune mise à jour en vue !" } }, "it" : { @@ -12521,8 +12533,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "This is not the update you're looking for!" + "state" : "translated", + "value" : "Ça n'est pas la mise à jour que vous recherchez !" } }, "it" : { @@ -12545,7 +12557,7 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Essa não é a atualização que você está procurando." } }, @@ -12598,8 +12610,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "We've misplaced the 'Update' button. Oops!" + "state" : "translated", + "value" : "On a égaré le bouton “Mettre à jour”. Oups !" } }, "it" : { @@ -12669,14 +12681,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Juro que estaba aquí en alguna parte ... un segundo" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "I swear it was here somewhere… one sec" + "state" : "translated", + "value" : "Je te jure que c’était là… une seconde." } }, "it" : { @@ -12752,8 +12764,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "An apple a day keeps the… updates away." + "state" : "translated", + "value" : "Une pomme par jour éloigne les… mises à jour pour toujours." } }, "it" : { @@ -12829,8 +12841,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "May the Force be with you… next time!" + "state" : "translated", + "value" : "Que la Force soit avec vous… la prochaine fois !" } }, "it" : { @@ -12906,8 +12918,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "The Force is strong with this version!" + "state" : "translated", + "value" : "La Force est puissante avec cette version !" } }, "it" : { @@ -12930,7 +12942,7 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "A Força é potente nessa versão." } }, @@ -12983,9 +12995,9 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Just a small town app, same old version" - } + "state" : "translated", + "value" : "-" + } }, "it" : { "stringUnit" : { @@ -13060,8 +13072,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Winter is coming. Updates aren't yet." + "state" : "translated", + "value" : "L’hiver arrive. Les mises à jour, pas encore." } }, "it" : { @@ -13090,8 +13102,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Зима близко. Но обновления нет" + "state" : "translated", + "value" : "Зима близко. Но обновления нет." } }, "zh-Hans" : { @@ -13131,14 +13143,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Dulces sueños están hechos de... no actualizar." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Sweet dreams are made of… no updates" + "state" : "translated", + "value" : "-" } }, "it" : { @@ -13161,7 +13173,7 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Sonho meu, sonho meu... sem updates como o céu!" } }, @@ -13214,8 +13226,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "The update fairy skipped us this week." + "state" : "translated", + "value" : "La fée des mises à jour nous a oubliés cette semaine." } }, "it" : { @@ -13291,8 +13303,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Stay sharp, more intel coming soon!" + "state" : "translated", + "value" : "Restez vigilant, d’autres infos arrivent bientôt !" } }, "it" : { @@ -13362,14 +13374,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Estás navegando con la última tecnología." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You're cruising on the latest tech!" + "state" : "translated", + "value" : "Vous êtes déjà à la pointe de la technologie !" } }, "it" : { @@ -13445,8 +13457,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "We’ll be back. With updates… later" + "state" : "translated", + "value" : "Nous reviendrons. Avec des mises à jour… un jour" } }, "it" : { @@ -13516,14 +13528,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Una moneda por tus... faltas de actualizaciones" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "A penny for your… lack of updates." + "state" : "translated", + "value" : "Un sou pour ton… absence de mises à jour." } }, "it" : { @@ -13599,8 +13611,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You've already got the best Loop!" + "state" : "translated", + "value" : "Vous avez déjà la meilleure version de Loop !" } }, "it" : { @@ -13623,7 +13635,7 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Você já está com o melhor Loop." } }, @@ -13676,8 +13688,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "One does not simply update Loop." + "state" : "translated", + "value" : "On ne met pas à jour Loop si facilement." } }, "it" : { @@ -13747,14 +13759,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Mucho trabajar y poco actualizar..." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "All work and no… no updates…" + "state" : "translated", + "value" : "-" } }, "it" : { @@ -13777,14 +13789,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Todo esse trabalho e... sem atualizações..." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Только работа и никаких.. обновлений..." + "state" : "translated", + "value" : "Только работа и никаких… обновлений…" } }, "zh-Hans" : { @@ -13830,8 +13842,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "A watched pot never… updates." + "state" : "translated", + "value" : "Tout vient à point à qui sait attendre... les mises à jour." } }, "it" : { @@ -13907,8 +13919,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "99 problems, updates ain't one." + "state" : "translated", + "value" : "J’ai 99 problèmes, mais les mises à jour n’en font pas partie." } }, "it" : { @@ -13937,8 +13949,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "99 проблем, ни одного обновления" + "state" : "translated", + "value" : "99 проблем, ни одного обновления." } }, "zh-Hans" : { @@ -13978,14 +13990,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Debi haberla dejado por aquí..." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "I… uhh… one sec I lost it" + "state" : "translated", + "value" : "Euh… une seconde… j’ai perdu le fil" } }, "it" : { @@ -14061,8 +14073,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You’ve leveled up to the max!" + "state" : "translated", + "value" : "Vous avez atteint le niveau max !" } }, "it" : { @@ -14132,14 +14144,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Colega, ¿dónde está mi actualización?" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Beggars can't be… updaters." + "state" : "translated", + "value" : "-" } }, "it" : { @@ -14209,14 +14221,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Podrá tener todo el dinero que quiera, pero no tiene una actualización" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Money can't buy… updates." + "state" : "translated", + "value" : "L’argent ne peut pas acheter… de mises à jour." } }, "it" : { @@ -14245,8 +14257,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Обновления бесценны..." + "state" : "translated", + "value" : "Обновления бесценны…" } }, "zh-Hans" : { @@ -14292,8 +14304,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "No new intel, Commander." + "state" : "translated", + "value" : "Rien à signaler, Commandant." } }, "it" : { @@ -14322,8 +14334,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Ничего нового, сэр" + "state" : "translated", + "value" : "Ничего нового, сэр." } }, "zh-Hans" : { @@ -14363,14 +14375,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "No actualizaciones? Gran Scott" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "No updates? Great Scott!" + "state" : "translated", + "value" : "Pas de mises à jour ? Nom de Zeus !" } }, "it" : { @@ -14440,14 +14452,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "No hay actualizaciones, señor Anderson..." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "No updates, Mr. Anderson" + "state" : "translated", + "value" : "Pas de mise à jour, Mr Anderson" } }, "it" : { @@ -14517,14 +14529,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Negativo, camarada." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "No updates in Ba Sing Se" + "state" : "translated", + "value" : "Pas de mises à jour à Ba Sing Se" } }, "it" : { @@ -14600,8 +14612,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Updates? In this economy?" + "state" : "translated", + "value" : "Des mises à jour ? Avec la conjoncture actuelle ?" } }, "it" : { @@ -14677,8 +14689,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Check back next time!" + "state" : "translated", + "value" : "Revenez plus tard !" } }, "it" : { @@ -14748,14 +14760,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Loop está mamadísimo" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Loop is in its prime!" + "state" : "translated", + "value" : "Loop fonctionne à plein régime !" } }, "it" : { @@ -14831,8 +14843,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "All systems are a-go!" + "state" : "translated", + "value" : "Tous les systèmes sont opérationnels !" } }, "it" : { @@ -14902,14 +14914,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Estás actualizado!" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You're up to date :)" + "state" : "translated", + "value" : "Vous êtes à jour :)" } }, "it" : { @@ -14985,8 +14997,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "No updates yet!" + "state" : "translated", + "value" : "Aucune mise à jour pour le moment !" } }, "it" : { @@ -15038,7 +15050,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "None" } }, @@ -15062,8 +15074,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "None" + "state" : "translated", + "value" : "Non défini" } }, "it" : { @@ -15139,8 +15151,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Nothing to cycle through" + "state" : "translated", + "value" : "La séquence est vide" } }, "it" : { @@ -15192,7 +15204,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%@'s notification permissions are currently disabled." } }, @@ -15216,8 +15228,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "%@'s notification permissions are currently disabled." + "state" : "translated", + "value" : "%@ n’a pas l’autorisation d’envoyer des notifications pour le moment." } }, "it" : { @@ -15246,8 +15258,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Вы не дали %@ разрешение на отправку уведомлений" + "state" : "translated", + "value" : "Вы не дали %@ разрешение на отправку уведомлений." } }, "zh-Hans" : { @@ -15293,8 +15305,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Open Settings" + "state" : "translated", + "value" : "Ouvrir les réglages" } }, "it" : { @@ -15346,7 +15358,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Please turn them on in System Settings." } }, @@ -15370,8 +15382,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Please turn them on in System Settings." + "state" : "translated", + "value" : "Activez-les dans les réglages système." } }, "it" : { @@ -15447,8 +15459,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Set Current Icon" + "state" : "translated", + "value" : "Utiliser cette icône" } }, "it" : { @@ -15524,8 +15536,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Notify when unlocking new icons" + "state" : "translated", + "value" : "Avertir quand de nouvelles icônes sont débloquées" } }, "it" : { @@ -15595,13 +15607,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "OK" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "OK" } }, @@ -15625,7 +15637,7 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "OK" } }, @@ -15678,7 +15690,7 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Options" } }, @@ -15731,7 +15743,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Padding" } }, @@ -15755,8 +15767,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Padding" + "state" : "translated", + "value" : "Marge" } }, "it" : { @@ -15808,7 +15820,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Permissions" } }, @@ -15832,7 +15844,7 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Permissions" } }, @@ -15885,7 +15897,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Please press deny when Loop \n requests screen recording permissions." } }, @@ -15903,14 +15915,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Please press deny when Loop \n requests screen recording permissions." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Please press deny when Loop \n requests screen recording permissions." + "state" : "translated", + "value" : "Veuillez cliquer sur Refuser lorsque Loop \n demande l’autorisation pour enregistrer l’écran." } }, "it" : { @@ -15927,13 +15939,13 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Please press deny when Loop \n requests screen recording permissions." } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Please press deny when Loop \n requests screen recording permissions." } }, @@ -15986,7 +15998,7 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Position" } }, @@ -16063,8 +16075,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Preserve Size" + "state" : "translated", + "value" : "Conserver les dimensions" } }, "it" : { @@ -16116,7 +16128,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Press \"Add\" to add a cycle item" } }, @@ -16134,14 +16146,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Presiona \"añadir\" para añadir a la rotación " } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Press \"Add\" to add a cycle item" + "state" : "translated", + "value" : "Appuyez sur \"Ajouter\" pour ajouter un élément à la séquence." } }, "it" : { @@ -16211,14 +16223,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Presiona añadir para añadir una combinación de teclas " } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Press \"Add\" to add a keybind" + "state" : "translated", + "value" : "Appuyez sur \"Ajouter\" pour ajouter un raccourci" } }, "it" : { @@ -16294,8 +16306,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Press \"Add\" to add an application" + "state" : "translated", + "value" : "Appuyez sur \"Ajouter\" pour ajouter une application" } }, "it" : { @@ -16371,8 +16383,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Preview" + "state" : "translated", + "value" : "Aperçu" } }, "it" : { @@ -16442,13 +16454,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "px" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "px" } }, @@ -16466,19 +16478,19 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "px" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "px" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "px" } }, @@ -16525,8 +16537,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Quarters" + "state" : "translated", + "value" : "Quart" } }, "it" : { @@ -16602,8 +16614,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Quit" + "state" : "translated", + "value" : "Quitter" } }, "it" : { @@ -16679,8 +16691,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Radial menu" + "state" : "translated", + "value" : "Menu radial" } }, "it" : { @@ -16750,14 +16762,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Radial Menu" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Radial Menu" + "state" : "translated", + "value" : "Menu Radial" } }, "it" : { @@ -16833,8 +16845,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Red" + "state" : "translated", + "value" : "Rouge" } }, "it" : { @@ -16882,7 +16894,14 @@ } }, "Relaxed" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "느긋한" + } + } + } }, "Remind me later" : { "extractionState" : "manual", @@ -16913,8 +16932,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Remind me later" + "state" : "translated", + "value" : "Me le rappeler plus tard" } }, "it" : { @@ -16990,8 +17009,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Remove" + "state" : "translated", + "value" : "Supprimer" } }, "it" : { @@ -17061,14 +17080,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Solicitar..." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Request…" + "state" : "translated", + "value" : "Autoriser" } }, "it" : { @@ -17091,14 +17110,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Solicitar..." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Запросить..." + "state" : "translated", + "value" : "Запросить…" } }, "zh-Hans" : { @@ -17144,8 +17163,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Reset" + "state" : "translated", + "value" : "Réinitialiser" } }, "it" : { @@ -17221,8 +17240,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Resize window under cursor" + "state" : "translated", + "value" : "Redimensionner la fenêtre sous le curseur" } }, "it" : { @@ -17298,8 +17317,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Respect Stage Manager" + "state" : "translated", + "value" : "Gérer la compatibilité avec Stage Manage" } }, "it" : { @@ -17351,7 +17370,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Restart to complete" } }, @@ -17375,8 +17394,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Restart to complete" + "state" : "translated", + "value" : "Redémarrer pour terminer" } }, "it" : { @@ -17452,8 +17471,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Restore window frame on drag" + "state" : "translated", + "value" : "Restaurer le cadre de la fenêtre lors du glissement" } }, "it" : { @@ -17529,8 +17548,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Right" + "state" : "translated", + "value" : "Droit" } }, "it" : { @@ -17607,8 +17626,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Right %@" + "state" : "translated", + "value" : "%@ droit" } }, "it" : { @@ -17684,8 +17703,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Right Cycle" + "state" : "translated", + "value" : "Séquence droite" } }, "it" : { @@ -17737,7 +17756,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Screen capture access" } }, @@ -17755,14 +17774,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Screen capture access" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Screen capture access" + "state" : "translated", + "value" : "Accès à l'enregistrement d'écran" } }, "it" : { @@ -17773,26 +17792,26 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Screen capture access" + "state" : "translated", + "value" : "화면 캡처 접근 권한" } }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Screen capture access" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Screen capture access" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Screen capture access" + "state" : "translated", + "value" : "Доступ к записи экрана" } }, "zh-Hans" : { @@ -17814,7 +17833,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Please grant access to be able to dynamically set Loop's accent color based on your wallpaper. Please relaunch %@ after granting access." } }, @@ -17832,14 +17851,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Please grant access to be able to dynamically set Loop's accent color based on your wallpaper. Please relaunch %@ after granting access." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Please grant access to be able to dynamically set Loop's accent color based on your wallpaper. Please relaunch %@ after granting access." + "state" : "translated", + "value" : "Merci d’accorder l’accès afin que Loop puisse adapter dynamiquement sa couleur d’accentuation à votre fond d’écran. Relancez %@ après avoir autorisé l’accès." } }, "it" : { @@ -17850,26 +17869,26 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Please grant access to be able to dynamically set Loop's accent color based on your wallpaper. Please relaunch %@ after granting access." + "state" : "translated", + "value" : "배경화면에 따라 Loop의 강조 색상을 동적으로 설정할 수 있도록 접근 권한을 부여해주세요. 접근 권한을 부여한 후 %@을(를) 다시 실행해주세요." } }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Please grant access to be able to dynamically set Loop's accent color based on your wallpaper. Please relaunch %@ after granting access." } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Please grant access to be able to dynamically set Loop's accent color based on your wallpaper. Please relaunch %@ after granting access." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Please grant access to be able to dynamically set Loop's accent color based on your wallpaper. Please relaunch %@ after granting access." + "state" : "translated", + "value" : "Пожалуйста, выдайте доступ, чтобы вы могли динамически устанавливать акцентный цвет для Loop основываясь на ваших обоях. Пожалуйста, перезапустите %@ после выдачи доступа." } }, "zh-Hans" : { @@ -17891,7 +17910,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%@ Needs Screen Recording Permissions" } }, @@ -17909,14 +17928,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%@ Needs Screen Recording Permissions" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "%@ Needs Screen Recording Permissions" + "state" : "translated", + "value" : "%@ nécessite l'autorisation d’enregistrement de l’écran" } }, "it" : { @@ -17927,26 +17946,26 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "%@ Needs Screen Recording Permissions" + "state" : "translated", + "value" : "%@ 화면 녹화 권한이 필요합니다" } }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%@ Needs Screen Recording Permissions" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "%@ Needs Screen Recording Permissions" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "%@ Needs Screen Recording Permissions" + "state" : "translated", + "value" : "%@ Нужно разрешение на запись экрана" } }, "zh-Hans" : { @@ -17992,8 +18011,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Screen Switching" + "state" : "translated", + "value" : "Changement d'écran" } }, "it" : { @@ -18045,7 +18064,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Search…" } }, @@ -18063,14 +18082,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Search…" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Search…" + "state" : "translated", + "value" : "Rechercher…" } }, "it" : { @@ -18093,14 +18112,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Search…" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Search…" + "state" : "translated", + "value" : "Поиск…" } }, "zh-Hans" : { @@ -18118,11 +18137,10 @@ } }, "Select a keybinds file" : { - "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Select a keybinds file" } }, @@ -18132,22 +18150,16 @@ "value" : "Wähle eine Keybind-Datei aus" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Select a keybinds file" - } - }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Select a keybinds file" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Select a keybinds file" + "state" : "translated", + "value" : "Sélectionner un fichier de raccourcis" } }, "it" : { @@ -18158,26 +18170,26 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Select a keybinds file" + "state" : "translated", + "value" : "키바인드 파일 선택" } }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Select a keybinds file" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Select a keybinds file" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Select a keybinds file" + "state" : "translated", + "value" : "Выберите файл с горячими клавишами" } }, "zh-Hans" : { @@ -18223,8 +18235,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Select Loop keybinds file" + "state" : "translated", + "value" : "Sélectionner le fichier de raccourcis de Loop" } }, "it" : { @@ -18300,8 +18312,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Send Feedback" + "state" : "translated", + "value" : "Envoyer un feedback" } }, "it" : { @@ -18353,7 +18365,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "تعيين مفتاح التنشيط..." } }, @@ -18371,14 +18383,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Seleccionar tecla de activación" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Set a trigger key…" + "state" : "translated", + "value" : "Attribuer une touche d’activation" } }, "it" : { @@ -18401,14 +18413,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Defina uma tecla de gatilho..." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Установить клавишу активации..." + "state" : "translated", + "value" : "Установить клавишу активации…" } }, "zh-Hans" : { @@ -18454,8 +18466,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Settings" + "state" : "translated", + "value" : "Réglages" } }, "it" : { @@ -18507,7 +18519,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "حول" } }, @@ -18525,14 +18537,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Acerca de" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "About" + "state" : "translated", + "value" : "À Propos" } }, "it" : { @@ -18543,8 +18555,8 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "About" + "state" : "translated", + "value" : "정보" } }, "nl-BE" : { @@ -18555,13 +18567,13 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Sobre" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "О нас" } }, @@ -18584,7 +18596,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "لون التمييز" } }, @@ -18602,14 +18614,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Color de acento" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Accent Color" + "state" : "translated", + "value" : "Couleur d'accentuation" } }, "it" : { @@ -18620,8 +18632,8 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Accent Color" + "state" : "translated", + "value" : "강조 색상" } }, "nl-BE" : { @@ -18632,13 +18644,13 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Cor de Destaque" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Акцентный Цвет" } }, @@ -18661,7 +18673,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "متقدم" } }, @@ -18679,14 +18691,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Avanzado" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Advanced" + "state" : "translated", + "value" : "Avancé" } }, "it" : { @@ -18697,8 +18709,8 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Advanced" + "state" : "translated", + "value" : "고급" } }, "nl-BE" : { @@ -18709,13 +18721,13 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Avançado" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Дополнительные настройки" } }, @@ -18738,7 +18750,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "سلوك" } }, @@ -18756,14 +18768,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Comportamiento" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Behavior" + "state" : "translated", + "value" : "Comportement" } }, "it" : { @@ -18774,8 +18786,8 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Behavior" + "state" : "translated", + "value" : "동작" } }, "nl-BE" : { @@ -18786,13 +18798,13 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Comportamento" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Поведение" } }, @@ -18815,7 +18827,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "التطبيقات المستبعدة" } }, @@ -18833,14 +18845,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Apps excluidas" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Excluded Apps" + "state" : "translated", + "value" : "Applications ignorées" } }, "it" : { @@ -18851,8 +18863,8 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Excluded Apps" + "state" : "translated", + "value" : "제외된 앱" } }, "nl-BE" : { @@ -18863,13 +18875,13 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Aplicativos Excluídos" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Игнорировать приложения" } }, @@ -18892,7 +18904,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "أيقونة" } }, @@ -18910,14 +18922,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Icono" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Icon" + "state" : "translated", + "value" : "Icône" } }, "it" : { @@ -18928,8 +18940,8 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Icon" + "state" : "translated", + "value" : "아이콘" } }, "nl-BE" : { @@ -18940,13 +18952,13 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Ícone" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Иконка" } }, @@ -18969,7 +18981,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "روابط المفاتيح" } }, @@ -18987,14 +18999,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Asignaciones de teclas" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Keybinds" + "state" : "translated", + "value" : "Raccourcis" } }, "it" : { @@ -19005,8 +19017,8 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Keybinds" + "state" : "translated", + "value" : "키바인드" } }, "nl-BE" : { @@ -19017,13 +19029,13 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Combinações" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Сочетания клавиш" } }, @@ -19046,7 +19058,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "معاينة" } }, @@ -19064,14 +19076,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Previsualización" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Preview" + "state" : "translated", + "value" : "Aperçu" } }, "it" : { @@ -19082,8 +19094,8 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Preview" + "state" : "translated", + "value" : "미리보기" } }, "nl-BE" : { @@ -19094,13 +19106,13 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Prévia" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Превью" } }, @@ -19123,7 +19135,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "قائمة دائرية" } }, @@ -19141,14 +19153,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Radial Menu" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Radial Menu" + "state" : "translated", + "value" : "Menu Radial" } }, "it" : { @@ -19159,8 +19171,8 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "방사형 메뉴" + "state" : "translated", + "value" : "원형 메뉴 " } }, "nl-BE" : { @@ -19171,13 +19183,13 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Menu Radial" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Радиальное Меню" } }, @@ -19200,7 +19212,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "الإعدادات..." } }, @@ -19218,14 +19230,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Ajustes..." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Settings…" + "state" : "translated", + "value" : "Réglages…" } }, "it" : { @@ -19248,14 +19260,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Configurações..." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Настройки..." + "state" : "translated", + "value" : "Настройки…" } }, "zh-Hans" : { @@ -19301,8 +19313,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Share feedback on our GitHub page, where you can let us know about any bugs, suggest features, or provide other valuable input. We also accept donations if you feel that Loop has improved your workflow :)" + "state" : "translated", + "value" : "Partagez vos retours sur notre page GitHub : vous pouvez y signaler des bugs, suggérer des fonctionnalités ou nous faire part de vos idées. Si Loop a amélioré votre productivité, vous pouvez aussi nous soutenir par un don :)\"" } }, "it" : { @@ -19378,8 +19390,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Show in dock" + "state" : "translated", + "value" : "Afficher dans le dock" } }, "it" : { @@ -19455,8 +19467,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Show preview when looping" + "state" : "translated", + "value" : "Afficher un aperçu pendant l'utilisation" } }, "it" : { @@ -19508,7 +19520,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Shrink" } }, @@ -19526,14 +19538,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Shrink" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Shrink" + "state" : "translated", + "value" : "Réduire" } }, "it" : { @@ -19550,20 +19562,20 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Shrink" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Shrink" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Shrink" + "state" : "translated", + "value" : "Уменьшить" } }, "zh-Hans" : { @@ -19603,13 +19615,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Simple" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Simple" } }, @@ -19686,8 +19698,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Size" + "state" : "translated", + "value" : "Taille" } }, "it" : { @@ -19739,7 +19751,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Size Adjustment" } }, @@ -19757,14 +19769,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Size Adjustment" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Size Adjustment" + "state" : "translated", + "value" : "Ajustement de la taille" } }, "it" : { @@ -19781,20 +19793,20 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Size Adjustment" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Size Adjustment" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Size Adjustment" + "state" : "translated", + "value" : "Изменение размера" } }, "zh-Hans" : { @@ -19840,8 +19852,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Size increment" + "state" : "translated", + "value" : "Incrément de taille" } }, "it" : { @@ -19917,8 +19929,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Smooth" + "state" : "translated", + "value" : "Fluide" } }, "it" : { @@ -19966,7 +19978,14 @@ } }, "Snappy" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "딸깍 반응" + } + } + } }, "Some features, ideas, and bug fixes" : { "extractionState" : "manual", @@ -19997,8 +20016,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Some features, ideas, and bug fixes" + "state" : "translated", + "value" : "Fonctionnalités, idées et correctifs" } }, "it" : { @@ -20074,7 +20093,7 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Stage Manager" } }, @@ -20092,13 +20111,13 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Stage Manager" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Stage Manager" } }, @@ -20151,8 +20170,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Stage strip size" + "state" : "translated", + "value" : "Taille de la bande du Stage Manager" } }, "it" : { @@ -20228,8 +20247,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Suggest new icon" + "state" : "translated", + "value" : "Proposer une nouvelle icône" } }, "it" : { @@ -20305,8 +20324,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Sync Wallpaper" + "state" : "translated", + "value" : "Synchroniser avec le fond d'écran" } }, "it" : { @@ -20376,14 +20395,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Esta tecla está actualmente en uso como tecla de activación" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "That key is already used as your trigger key." + "state" : "translated", + "value" : "Cette touche est déjà utilisée en tant que touche d'activation." } }, "it" : { @@ -20453,14 +20472,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Esta combinación está siendo usada por %@" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "That keybind is already being used by %@." + "state" : "translated", + "value" : "Ce raccourci est déjà utilisé par %@" } }, "it" : { @@ -20530,14 +20549,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Esta combinación está siendo usada por otra combinación personalizada" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "That keybind is already being used by another custom keybind." + "state" : "translated", + "value" : "Ce raccourci est déjà utilisé par un autre raccourci personnalisé." } }, "it" : { @@ -20613,8 +20632,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Theming" + "state" : "translated", + "value" : "Thème" } }, "it" : { @@ -20662,11 +20681,10 @@ } }, "There are other keybinds that conflict with this key combination." : { - "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "There are other keybinds that conflict with this key combination." } }, @@ -20676,22 +20694,16 @@ "value" : "Es gibt andere Tastenkombinationen, die mit dieser in Konflikt stehen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "There are other keybinds that conflict with this key combination." - } - }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "There are other keybinds that conflict with this key combination." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "There are other keybinds that conflict with this key combination." + "state" : "translated", + "value" : "Cette combinaison est déjà utilisée par un autre raccourci." } }, "it" : { @@ -20702,26 +20714,26 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "There are other keybinds that conflict with this key combination." + "state" : "translated", + "value" : "이 키 조합과 충돌하는 다른 키바인드가 있습니다" } }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "There are other keybinds that conflict with this key combination." } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "There are other keybinds that conflict with this key combination." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "There are other keybinds that conflict with this key combination." + "state" : "translated", + "value" : "У вас уже есть другие назначения клавиш на эту комбинацию клавиш." } }, "zh-Hans" : { @@ -20743,7 +20755,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "السماكة\n\n" } }, @@ -20767,8 +20779,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Thickness" + "state" : "translated", + "value" : "Épaisseur" } }, "it" : { @@ -20838,14 +20850,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Esta funcionalidad aún está en desarrollo" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "This feature is still under development." + "state" : "translated", + "value" : "Cette fonctionnalité est encore en cours de développement." } }, "it" : { @@ -20921,8 +20933,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Top" + "state" : "translated", + "value" : "Haut" } }, "it" : { @@ -20939,7 +20951,7 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Top" } }, @@ -20998,8 +21010,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Top Cycle" + "state" : "translated", + "value" : "Séquence haute" } }, "it" : { @@ -21075,8 +21087,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Trigger delay" + "state" : "translated", + "value" : "Délai d'activation" } }, "it" : { @@ -21152,8 +21164,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Trigger Key" + "state" : "translated", + "value" : "Touche d'activation" } }, "it" : { @@ -21223,14 +21235,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Actualizar..." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Update…" + "state" : "translated", + "value" : "Mise à jour…" } }, "it" : { @@ -21253,14 +21265,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Atualizar..." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Обновление..." + "state" : "translated", + "value" : "Обновление…" } }, "zh-Hans" : { @@ -21278,7 +21290,14 @@ } }, "Updates are disabled" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "업데이트가 비활성화되었습니다" + } + } + } }, "Use coordinates" : { "extractionState" : "manual", @@ -21309,8 +21328,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Use coordinates" + "state" : "translated", + "value" : "Utiliser des coordonnées" } }, "it" : { @@ -21386,8 +21405,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Use macOS center" + "state" : "translated", + "value" : "Utiliser le centre selon macOS" } }, "it" : { @@ -21439,7 +21458,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Use macOS window manager when available" } }, @@ -21457,14 +21476,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Use macOS window manager when available" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Use macOS window manager when available" + "state" : "translated", + "value" : "Utiliser le gestionnaire de fenêtres de macOS si possible" } }, "it" : { @@ -21475,26 +21494,26 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Use macOS window manager when available" + "state" : "translated", + "value" : "사용 가능한 경우 macOS 창 관리자 사용" } }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Use macOS window manager when available" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Use macOS window manager when available" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Use macOS window manager when available" + "state" : "translated", + "value" : "Использовать оконный менеджер macOS" } }, "zh-Hans" : { @@ -21540,8 +21559,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Use pixels" + "state" : "translated", + "value" : "Valeurs fixes en pixels" } }, "it" : { @@ -21611,14 +21630,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Usa esto si estás utilizando una barra de menú personalizada" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Use this if you are using a custom menubar." + "state" : "translated", + "value" : "À utiliser si vous avez une barre de menu personnalisée." } }, "it" : { @@ -21647,8 +21666,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Включите это, если вы не используете встроенную строку меню" + "state" : "translated", + "value" : "Включите это, если вы не используете встроенную строку меню." } }, "zh-Hans" : { @@ -21670,7 +21689,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Version %1$@ (%2$lld)" } }, @@ -21694,7 +21713,7 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Version %1$@ (%2$lld)" } }, @@ -21724,8 +21743,8 @@ }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Версия %1$@ (%$11d)" + "state" : "translated", + "value" : "Версия %1$@ (%2$lld)" } }, "zh-Hans" : { @@ -21771,8 +21790,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Vertical Thirds" + "state" : "translated", + "value" : "Tiers verticaux" } }, "it" : { @@ -21848,8 +21867,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Width" + "state" : "translated", + "value" : "Largeur" } }, "it" : { @@ -21925,8 +21944,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Window" + "state" : "translated", + "value" : "Fenêtre" } }, "it" : { @@ -22002,8 +22021,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Almost Maximize" + "state" : "translated", + "value" : "Presque maximiser" } }, "it" : { @@ -22079,8 +22098,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Bottom Half" + "state" : "translated", + "value" : "Moitié basse" } }, "it" : { @@ -22156,8 +22175,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Bottom Left Quarter" + "state" : "translated", + "value" : "Quart inférieur gauche" } }, "it" : { @@ -22233,8 +22252,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Bottom Right Quarter" + "state" : "translated", + "value" : "Quart inférieur droit" } }, "it" : { @@ -22310,8 +22329,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Bottom Third" + "state" : "translated", + "value" : "Tiers bas" } }, "it" : { @@ -22387,8 +22406,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Bottom Two Thirds" + "state" : "translated", + "value" : "Deux tiers inférieurs" } }, "it" : { @@ -22464,8 +22483,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Center" + "state" : "translated", + "value" : "Centrer" } }, "it" : { @@ -22541,8 +22560,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Custom" + "state" : "translated", + "value" : "Personnalisé" } }, "it" : { @@ -22618,8 +22637,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Cycle" + "state" : "translated", + "value" : "Séquence" } }, "it" : { @@ -22695,8 +22714,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Fullscreen" + "state" : "translated", + "value" : "Plein écran" } }, "it" : { @@ -22772,8 +22791,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Grow Bottom" + "state" : "translated", + "value" : "Agrandir en bas" } }, "it" : { @@ -22849,8 +22868,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Grow Left" + "state" : "translated", + "value" : "Agrandir à gauche" } }, "it" : { @@ -22926,8 +22945,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Grow Right" + "state" : "translated", + "value" : "Agrandir à droite" } }, "it" : { @@ -23003,8 +23022,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Grow Top" + "state" : "translated", + "value" : "Agrandir en haut" } }, "it" : { @@ -23080,8 +23099,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Hide" + "state" : "translated", + "value" : "Cacher" } }, "it" : { @@ -23133,7 +23152,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Horizontal Center Half" } }, @@ -23151,14 +23170,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Horizontal Center Half" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Horizontal Center Half" + "state" : "translated", + "value" : "Moitié centrale horizontale" } }, "it" : { @@ -23169,26 +23188,26 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Horizontal Center Half" + "state" : "translated", + "value" : "수평 중앙 절반" } }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Horizontal Center Half" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Horizontal Center Half" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Horizontal Center Half" + "state" : "translated", + "value" : "Горизонтальная половина по центру" } }, "zh-Hans" : { @@ -23234,8 +23253,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Horizontal Center Third" + "state" : "translated", + "value" : "Tiers central horizontal" } }, "it" : { @@ -23311,8 +23330,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Initial Frame" + "state" : "translated", + "value" : "Dimensions initiales" } }, "it" : { @@ -23388,8 +23407,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Larger" + "state" : "translated", + "value" : "Agrandir" } }, "it" : { @@ -23465,8 +23484,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Left Half" + "state" : "translated", + "value" : "Moitié gauche" } }, "it" : { @@ -23542,8 +23561,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Left Third" + "state" : "translated", + "value" : "Tiers gauche" } }, "it" : { @@ -23619,8 +23638,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Left Two Thirds" + "state" : "translated", + "value" : "Deux tiers gauches" } }, "it" : { @@ -23690,14 +23709,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Centro de macOS" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "macOS Center" + "state" : "translated", + "value" : "Centre (macOS)" } }, "it" : { @@ -23720,7 +23739,7 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Centro do macOS" } }, @@ -23773,8 +23792,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Maximize" + "state" : "translated", + "value" : "Maximiser" } }, "it" : { @@ -23822,11 +23841,11 @@ } }, "Window Direction/Name: Maximize Height" : { - "extractionState" : "manual", + "extractionState" : "extracted_with_value", "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Maximize Height" } }, @@ -23838,20 +23857,20 @@ }, "en" : { "stringUnit" : { - "state" : "translated", + "state" : "new", "value" : "Maximize Height" } }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Maximize Height" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Maximize Height" + "state" : "translated", + "value" : "Hauteur max" } }, "it" : { @@ -23862,26 +23881,26 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Maximize Height" + "state" : "translated", + "value" : "높이 최대화" } }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Maximize Height" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Maximize Height" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Maximize Height" + "state" : "translated", + "value" : "Максимизировать высоту" } }, "zh-Hans" : { @@ -23899,11 +23918,11 @@ } }, "Window Direction/Name: Maximize Width" : { - "extractionState" : "manual", + "extractionState" : "extracted_with_value", "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Maximize Width" } }, @@ -23915,20 +23934,20 @@ }, "en" : { "stringUnit" : { - "state" : "translated", + "state" : "new", "value" : "Maximize Width" } }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Maximize Width" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Maximize Width" + "state" : "translated", + "value" : "Largeur max" } }, "it" : { @@ -23939,26 +23958,26 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Maximize Width" + "state" : "translated", + "value" : "너비 최대화" } }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Maximize Width" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Maximize Width" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Maximize Width" + "state" : "translated", + "value" : "Максимизировать ширину" } }, "zh-Hans" : { @@ -24004,8 +24023,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Minimize" + "state" : "translated", + "value" : "Minimiser" } }, "it" : { @@ -24081,8 +24100,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Move Down" + "state" : "translated", + "value" : "Déplacer en bas" } }, "it" : { @@ -24158,8 +24177,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Move Left" + "state" : "translated", + "value" : "Déplacer à droite" } }, "it" : { @@ -24235,8 +24254,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Move Right" + "state" : "translated", + "value" : "Déplacer à droite" } }, "it" : { @@ -24312,8 +24331,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Move Up" + "state" : "translated", + "value" : "Déplacer en haut" } }, "it" : { @@ -24389,8 +24408,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Next Screen" + "state" : "translated", + "value" : "Écran suivant" } }, "it" : { @@ -24466,8 +24485,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "No Action" + "state" : "translated", + "value" : "Pas d'action" } }, "it" : { @@ -24543,8 +24562,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Previous Screen" + "state" : "translated", + "value" : "Écran précédent" } }, "it" : { @@ -24620,8 +24639,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Right Half" + "state" : "translated", + "value" : "Moitié droite" } }, "it" : { @@ -24697,8 +24716,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Right Third" + "state" : "translated", + "value" : "Tiers droit" } }, "it" : { @@ -24774,8 +24793,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Right Two Thirds" + "state" : "translated", + "value" : "Deux tiers droits" } }, "it" : { @@ -24851,8 +24870,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Shrink Bottom" + "state" : "translated", + "value" : "Réduire en bas" } }, "it" : { @@ -24928,8 +24947,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Shrink Left" + "state" : "translated", + "value" : "Réduire à gauche" } }, "it" : { @@ -25005,8 +25024,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Shrink Right" + "state" : "translated", + "value" : "Réduire à droite" } }, "it" : { @@ -25082,8 +25101,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Shrink Top" + "state" : "translated", + "value" : "Réduire en haut" } }, "it" : { @@ -25159,8 +25178,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Smaller" + "state" : "translated", + "value" : "Réduire" } }, "it" : { @@ -25236,8 +25255,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Top Half" + "state" : "translated", + "value" : "Moitié haute" } }, "it" : { @@ -25313,8 +25332,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Top Left Quarter" + "state" : "translated", + "value" : "Quart supérieur gauche" } }, "it" : { @@ -25390,8 +25409,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Top Right Quarter" + "state" : "translated", + "value" : "Quart supérieur droit" } }, "it" : { @@ -25467,8 +25486,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Top Third" + "state" : "translated", + "value" : "Tiers haut" } }, "it" : { @@ -25544,8 +25563,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Top Two Thirds" + "state" : "translated", + "value" : "Deux tiers supérieurs" } }, "it" : { @@ -25621,8 +25640,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Undo" + "state" : "translated", + "value" : "Annuler" } }, "it" : { @@ -25674,7 +25693,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Vertical Center Half" } }, @@ -25692,14 +25711,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Vertical Center Half" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Vertical Center Half" + "state" : "translated", + "value" : "Moitié centrale verticale" } }, "it" : { @@ -25710,26 +25729,26 @@ }, "ko" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Vertical Center Half" + "state" : "translated", + "value" : "수직 중앙 절반" } }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Vertical Center Half" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Vertical Center Half" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Vertical Center Half" + "state" : "translated", + "value" : "Вертикальная половина по центру" } }, "zh-Hans" : { @@ -25775,8 +25794,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Vertical Center Third" + "state" : "translated", + "value" : "Tiers central vertical" } }, "it" : { @@ -25852,8 +25871,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Window gaps" + "state" : "translated", + "value" : "Espacement entre les fenêtres" } }, "it" : { @@ -25929,8 +25948,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Window snapping" + "state" : "translated", + "value" : "Magnétisme des fenêtres" } }, "it" : { @@ -25947,7 +25966,7 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Window snapping" } }, @@ -26000,14 +26019,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Se previsualizará el ajuste de ventanas" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Window snapping will still use the preview." + "state" : "translated", + "value" : "Le magnétisme des fenêtres utilisera toujours l'aperçu." } }, "it" : { @@ -26077,13 +26096,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "X" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "X" } }, @@ -26101,19 +26120,19 @@ }, "nl-BE" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "X" } }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "X" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "X" } }, @@ -26154,13 +26173,13 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Y" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Y" } }, @@ -26184,13 +26203,13 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "new", "value" : "Y" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Y" } }, @@ -26231,14 +26250,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Solo se pueden usar hasta %lld teclas en una combinación, incluyendo la tecla de activación" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You can only use up to %lld keys in a keybind, including the trigger key." + "state" : "translated", + "value" : "Vous pouvez utiliser un maximum de %lld touches par raccourci, touches d'activation incluses." } }, "it" : { @@ -26308,14 +26327,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Solo se pueden usar hasta %lld teclas de activación" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You can only use up to %lld keys in your trigger key." + "state" : "translated", + "value" : "Vous pouvez utiliser un maximum de %lld touches d'activation." } }, "it" : { @@ -26391,8 +26410,8 @@ }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You've looped %lld times!" + "state" : "translated", + "value" : "Vous avez utilisé Loop %lld fois !" } }, "it" : { @@ -26444,7 +26463,7 @@ "localizations" : { "ar" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "لقد كررت... آه... لقد... فقدت العد..." } }, @@ -26462,14 +26481,14 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Has" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "You've looped… uhh… I… lost count…" + "state" : "translated", + "value" : "Vous avez bouclé… euh… j’ai perdu le compte…" } }, "it" : { @@ -26492,14 +26511,14 @@ }, "pt-BR" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Você fez... hmm... Eu... Perdi as contas..." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Я Loop'ил... эмм... я... я запутался..." + "state" : "translated", + "value" : "Я Loop'ил… эмм… я… я запутался…" } }, "zh-Hans" : { diff --git a/Loop/Luminare/Loop/AdvancedConfiguration.swift b/Loop/Luminare/Loop/AdvancedConfiguration.swift index cf19dbb3..94029607 100644 --- a/Loop/Luminare/Loop/AdvancedConfiguration.swift +++ b/Loop/Luminare/Loop/AdvancedConfiguration.swift @@ -47,7 +47,6 @@ class AdvancedConfigurationModel: ObservableObject { @Published var didResetSuccessfullyAlert = false @Published var isAccessibilityAccessGranted = AccessibilityManager.getStatus() - @Published var isScreenCaptureAccessGranted = ScreenCaptureManager.getStatus() @Published var accessibilityChecker: Publishers.Autoconnect = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @Published var accessibilityChecks: Int = 0 @@ -224,7 +223,6 @@ struct AdvancedConfigurationView: View { func permissionsSection() -> some View { LuminareSection("Permissions") { accessibilityComponent() - screenCaptureComponent() } .onReceive(model.accessibilityChecker) { _ in model.refreshAccessiblityStatus() @@ -256,30 +254,4 @@ struct AdvancedConfigurationView: View { .padding(.trailing, 2) .frame(height: elementHeight) } - - func screenCaptureComponent() -> some View { - HStack { - if model.isScreenCaptureAccessGranted { - Image(._18PxBadgeCheck2) - .foregroundStyle(tintColor()) - } - - Text("Screen capture access") - - Spacer() - - Button { - ScreenCaptureManager.requestAccess() - } label: { - Text("Request…") - .frame(height: 30) - .padding(.horizontal, 8) - } - .disabled(model.isScreenCaptureAccessGranted) - .buttonStyle(LuminareCompactButtonStyle(extraCompact: true)) - } - .padding(.leading, 8) - .padding(.trailing, 2) - .frame(height: elementHeight) - } } diff --git a/Loop/Luminare/Theming/WallpaperColors.swift b/Loop/Luminare/Theming/WallpaperColors.swift index ef91f113..e3cddb8d 100644 --- a/Loop/Luminare/Theming/WallpaperColors.swift +++ b/Loop/Luminare/Theming/WallpaperColors.swift @@ -6,94 +6,174 @@ // import Defaults -import ScreenCaptureKit import SwiftUI +// MARK: - Wallpaper processor error + +/// Represents errors that can occur during wallpaper processing. +public enum WallpaperProcessorError: Error { + case screenshotFailed + case dominantColorsCalculationFailed + case noWallpaperWindowsFound + case wallpaperWindowCaptureFailed + case screenCaptureFailed + case imageResizeFailed + case bitmapCreationFailed +} + // MARK: - Wallpaper colour processor /// IMPORTANT: FOR THE COLOR EXTRACTION FEATURE TO FUNCTION AUTOMATICALLY WITH LOOP, IT'S CRUCIAL TO GRANT /// ACCESSIBILITY PERMISSIONS TO YOUR DEVELOPMENT VERSION OF LOOP. ADDITIONALLY, ENSURE THAT ANY PREVIOUS /// PERMISSIONS GRANTED TO OFFICIALLY SIGNED VERSIONS OF LOOP ARE REVOKED. WITHOUT THESE STEPS, LOOP WILL /// NOT BE ABLE TO AUTOMATICALLY FETCH WALLPAPER COLORS, AND YOU'LL BE LIMITED TO THE MANUAL EXTRACTION METHOD. +/// +/// This implementation provides an advanced color extraction algorithm that: +/// - Efficiently processes desktop wallpaper images to extract vibrant colors +/// - Prioritizes visually appealing accent colors over technically dominant ones +/// - Uses a multi-step fallback approach to ensure it works across different permission scenarios +/// - Incorporates intelligent filtering to avoid colors that would make poor UI accents +/// +/// The algorithm is optimized for performance while maintaining high-quality color results. // The real beans here (I don't like beans) extension NSImage { /// Calculates the dominant colors of the image asynchronously. /// - Returns: An array of NSColor representing the dominant colors, or nil if an error occurs. - /// Resizing the image to a smaller size improves performance by reducing the number of pixels that need to be analyzed. - /// NOTE: This function tends to return darker colors, which can be problematic with darker wallpapers. To address this, - /// a brightness threshold is applied to filter out excessively dark colors. Additionally, the function filters out colors - /// that are very similar to each other, such as #000000 and #010101, to ensure a more diverse and representative color palette. + /// Optimized to return only the top 2 most vibrant and visually distinct colors. + /// + /// This method prioritizes colors with high saturation and medium brightness to find + /// visually appealing accent colors suitable for UI themes. The algorithm: + /// 1. Resizes the image to improve performance + /// 2. Samples pixels (skipping every other pixel to improve speed) + /// 3. Uses a quantization technique to group similar colors + /// 4. Scores colors based on both frequency and visual quality (saturation and balanced brightness) + /// 5. Ensures the returned colors are visually distinct from each other + /// + /// The scoring system is designed to favor vibrant colors over dull ones, even if the + /// dull colors appear more frequently in the image. This approach works well for extracting + /// accent colors from wallpapers, which often have subtle variation in dominant colors. func calculateDominantColors() async -> [NSColor]? { - // Resize the image to a smaller size to improve performance of color calculation. + // Resize the image to a smaller size to improve performance let aspectRatio = size.width / size.height - // Changing the aspect ratio will make it faster, but can also make it less accurate. - // I recommend 100x100 or 200x200. - let resizedImage = resized(to: NSSize(width: 200 * aspectRatio, height: 200)) + let resizedImage = resized(to: NSSize(width: 100 * aspectRatio, height: 100)) - // Ensure we can get the CGImage and its data provider from the resized image. guard let resizedCGImage = resizedImage?.cgImage(forProposedRect: nil, context: nil, hints: nil), let dataProvider = resizedCGImage.dataProvider, let data = CFDataGetBytePtr(dataProvider.data) else { - NSLog("Error: Unable to get CGImage or its data provider from the resized image.") + NSLog("Error: \(WallpaperProcessorError.imageResizeFailed)") return nil } - // Calculate the number of bytes per pixel and per row to access pixel data correctly. let bytesPerPixel = resizedCGImage.bitsPerPixel / 8 let bytesPerRow = resizedCGImage.bytesPerRow let width = resizedCGImage.width let height = resizedCGImage.height - var colorCountMap = [NSColor: Int]() - // Iterate over each pixel to count color occurrences. - for y in 0 ..< height { - for x in 0 ..< width { + // Use a lower quantization level to better group similar colors + // The value of 32 provides enough color differentiation while still grouping similar shades + let quantizationLevel = 32.0 + + // Use a dictionary to count color occurrences + // We use integer keys for better performance compared to using NSColor as keys + var colorCounts = [Int: Int]() // [ColorKey: Count] + var colorMap = [Int: NSColor]() // [ColorKey: ActualColor] + + // Sample every 2nd pixel for better performance + // This significantly speeds up processing with minimal impact on accuracy + for y in stride(from: 0, to: height, by: 2) { + for x in stride(from: 0, to: width, by: 2) { let pixelData = Int(y * bytesPerRow + x * bytesPerPixel) + + let red = CGFloat(data[pixelData]) / 255.0 + let green = CGFloat(data[pixelData + 1]) / 255.0 + let blue = CGFloat(data[pixelData + 2]) / 255.0 let alpha = (bytesPerPixel == 4) ? CGFloat(data[pixelData + 3]) / 255.0 : 1.0 - // Create an NSColor instance for the current pixel using RGBA values. - var color = NSColor( - red: CGFloat(data[pixelData]) / 255.0, - green: CGFloat(data[pixelData + 1]) / 255.0, - blue: CGFloat(data[pixelData + 2]) / 255.0, - alpha: alpha - ) - // Apply a quantization method to the color to reduce the color space complexity. - color = color.quantized() - // Increment the count for this color in the map. - colorCountMap[color, default: 0] += 1 + + // Skip fully transparent pixels + if alpha < 0.1 { continue } + + // Simple quantization - this maps similar colors to the same key + // Converting to integers reduces memory usage and improves comparison speed + let quantizedRed = Int(round(red * quantizationLevel)) + let quantizedGreen = Int(round(green * quantizationLevel)) + let quantizedBlue = Int(round(blue * quantizationLevel)) + + // Create a unique key for this color + // Bit-shifting creates a compact, unique integer representation of the RGB value + let colorKey = (quantizedRed << 16) | (quantizedGreen << 8) | quantizedBlue + + // Increment the count for this color + colorCounts[colorKey, default: 0] += 1 + + // Store the original color if we haven't seen this key before + // This preserves the original color quality rather than using the quantized version + if colorMap[colorKey] == nil { + colorMap[colorKey] = NSColor(red: red, green: green, blue: blue, alpha: alpha) + } } } - // Filter out very dark colors based on a brightness threshold to avoid the dominance of dark shades. - /// If you're using the saturation value, you can adjust this to 0.3 to get less dark colors - /// again, but be aware of darker wallpapers. - let brightnessThreshold: CGFloat = 0.2 // Ensure that the chosen color won't be too dark. - /// Filtering by saturation is good for some cases; however, in the case of a black wallpaper, - /// it will return #ED5A53 & #873D39, which does not match black. - // let saturationThreshold: CGFloat = 0.1 // Try to ensure that the chosen color won't be bright white. - let filteredByBrightness = colorCountMap - .filter { $0.key.brightness > brightnessThreshold } - // .filter { $0.key.saturationComponent > saturationThreshold } - /// Using only the brightness will allow us to return close to perfect colors. - /// For example, the black wallpaper from above, which previously gave #ED5A53 & #873D39, - /// is now returning #555555 & #353535, which matches the black/gray grained wallpaper in - /// tests used. If you need a perfect method, this can work, but it is designed for - /// beautiful colors and pure speed. It's not a 1:1 accurate method, although, removing the - /// filtering and color sorting brings you pretty close to a 1:1 color match. - - // If all colors are dark and the filtered map is empty, fallback to the original map. - let finalColors = filteredByBrightness.isEmpty ? colorCountMap : filteredByBrightness - - // Sort the colors by occurrence to find the most dominant colors. - let sortedColors = finalColors.sorted { $0.value > $1.value }.map(\.key) - - // Further filter out colors that are too similar to each other to ensure a diverse color palette. - let distinctColors = filterSimilarColors(colors: sortedColors) - - return distinctColors + // Calculate color vibrancy (using a combination of saturation and brightness) + // More vibrant colors (saturated but not too dark/light) score higher + var colorScores = [Int: Double]() + for (colorKey, color) in colorMap { + let count = colorCounts[colorKey] ?? 0 + guard count > 0 else { continue } + + let hsbColor = color.usingColorSpace(.deviceRGB)! + let saturation = hsbColor.saturationComponent + let brightness = hsbColor.brightness + + // Skip colors that are too dark or too light + // Colors at the extreme ends of brightness tend to make poor accent colors + if brightness < 0.15 || brightness > 0.95 { + continue + } + + // Calculate a score that favors vibrant colors (high saturation) but not + // extreme brightness or darkness + // The formula penalizes colors far from medium brightness (0.5) + let vibrancyScore = saturation * (1.0 - abs(brightness - 0.5) * 1.5) + + // Final score combines color frequency with vibrancy + // This balances between common colors and visually appealing ones + let score = Double(count) * vibrancyScore + colorScores[colorKey] = score + } + + // Sort colors by score and get top colors + // We get more than 2 initially because some might be filtered out as too similar + let sortedColors = colorScores + .sorted { $0.value > $1.value } + .prefix(10) + .map { colorMap[$0.key]! } + + // Ensure colors are distinct enough from each other + // This prevents selecting variations of the same color + var finalColors: [NSColor] = [] + for color in sortedColors { + if finalColors.isEmpty || !finalColors.contains(where: { color.isSimilar(to: $0, threshold: 0.15) }) { + finalColors.append(color) + if finalColors.count >= 2 { + break + } + } + } + + // If we couldn't find distinct vibrant colors, return the top 2 by frequency + // This fallback ensures we always return something useful + if finalColors.count < 2 { + let topColors = colorCounts + .sorted { $0.value > $1.value } + .prefix(2) + .compactMap { colorMap[$0.key] } + return Array(topColors) + } + + return finalColors } /// Helper function to resize the image to a new size. @@ -106,7 +186,7 @@ extension NSImage { samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0 ) else { - NSLog("Error: Unable to create NSBitmapImageRep for new size.") + NSLog("Error: \(WallpaperProcessorError.bitmapCreationFailed)") return nil } bitmapRep.size = newSize @@ -119,43 +199,32 @@ extension NSImage { resizedImage.addRepresentation(bitmapRep) return resizedImage } - - /// Filters out similar colors from an array, leaving only distinct colors. - /// - Parameter colors: The array of NSColor to filter. - /// - Returns: An array of NSColor with similar colors removed. - private func filterSimilarColors(colors: [NSColor]) -> [NSColor] { - var uniqueColors = [NSColor]() - // Iterate through the colors to build an array of unique colors. - for color in colors { - var isSimilar = false - for existingColor in uniqueColors { - // Check if the current color is similar to any already in the array. - if color.isSimilar(to: existingColor) { - isSimilar = true - break - } - } - // If the color is not similar to any existing colors, add it to the array. - if !isSimilar { - uniqueColors.append(color) - // Stop if we have the top 2 unique colors. - if uniqueColors.count == 2 { break } - } - } - return uniqueColors - } } // MARK: - Wallpaper public function +/// Processes desktop wallpapers to extract colors for theming Loop. +/// This class provides methods to capture the current desktop wallpaper and extract +/// vibrant, visually appealing colors that can be used as accent colors in the UI. public class WallpaperProcessor { /// Fetches the latest wallpaper colors and updates the app's theme settings. + /// + /// This method: + /// 1. Captures the current wallpaper image + /// 2. Processes it to extract dominant colors + /// 3. Updates the app's accent color settings with the extracted colors + /// + /// The first (most vibrant) color is used as the primary accent color, while + /// the second color is used as a gradient/secondary color. This provides + /// a cohesive theme that matches the user's desktop environment. static func fetchLatestWallpaperColors() async { do { // Attempt to process the current wallpaper to get the dominant colors. let dominantColors = try await processCurrentWallpaper() // Sort the first two colors by their brightness + // Using brightness sorting ensures that the brighter color is used as the primary accent, + // which typically works better for UI elements that need good contrast let colors = dominantColors.prefix(2).sorted(by: { $0.brightness > $1.brightness }) // Update the custom accent color with the first dominant color or clear if none. @@ -169,17 +238,18 @@ public class WallpaperProcessor { } /// Processes the current wallpaper and returns the dominant colors. - /// - Throws: An error if the screenshot fails or dominant colors cannot be calculated. + /// - Throws: A WallpaperProcessorError if the screenshot fails or dominant colors cannot be calculated. /// - Returns: An array of NSColor representing the dominant colors. + /// + /// This method coordinates the wallpaper capture and color analysis process. + /// It first attempts to capture a screenshot of the desktop wallpaper, then + /// passes that image to the color analysis algorithm to extract vibrant, + /// visually distinct colors suitable for UI accents. private static func processCurrentWallpaper() async throws -> [NSColor] { // Take a screenshot of the main display. guard let screenshot = await takeScreenshot() else { // If taking a screenshot fails, throw an error. - throw NSError( - domain: "WallpaperProcessorError", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "Failed to take a screenshot of the desktop wallpaper."] - ) + throw WallpaperProcessorError.screenshotFailed } // Calculate the dominant colors from the screenshot. @@ -188,11 +258,7 @@ public class WallpaperProcessor { // Ensure that dominant colors are calculated and the array is not empty. guard let colors = dominantColors, !colors.isEmpty else { // If no colors are found, throw an error. - throw NSError( - domain: "WallpaperProcessorError", - code: 2, - userInfo: [NSLocalizedDescriptionKey: "Could not calculate the dominant colors."] - ) + throw WallpaperProcessorError.dominantColorsCalculationFailed } return colors @@ -200,75 +266,143 @@ public class WallpaperProcessor { /// Takes a screenshot of the main display. /// - Returns: An NSImage of the screenshot or nil if the operation fails. + /// + /// This method attempts to capture the desktop wallpaper using three approaches: + /// 1. First, it tries to find and capture the Dock's wallpaper window directly that matches our screen dimensions + /// 2. If that fails, it tries to capture any wallpaper window from the Dock (even if not on our exact screen) + /// 3. As a last resort, it falls back to capturing the entire screen + /// + /// The direct wallpaper capture is preferred as it gets only the wallpaper without desktop icons, + /// but requires accessibility permissions (this is accepted required for Loop, so it's fine). + /// The fallback ensures we still get colors even if permissions aren't granted. private static func takeScreenshot() async -> NSImage? { let screen = NSScreen.screenWithMouse ?? NSScreen.main ?? NSScreen.screens[0] + let screenFrame = screen.displayBounds - // Check the macOS version to decide the method for capturing the screen. - if #available(macOS 14, *) { - do { - NSLog("Using modern method for macOS 15 and later.") - // For macOS versions 14 and later, use the modern method, using ScreenCaptureKit. - return try await takeScreenshotModern(screen) - } catch { - NSLog("Failed to capture the desktop wallpaper using the modern method: \(error.localizedDescription)") - return nil - } - } else { - NSLog("Using existing method for macOS versions below 15.") - // For macOS versions below 14, use the existing method. - return takeScreenshotOld(screen) + // First try to get the wallpaper window from the Dock app that matches our screen dimensions + if let wallpaperImage = try? await captureWallpaperFromDock(screenFrame: screenFrame, matchFrame: true) { + return wallpaperImage } - } - @available(macOS 14, *) - private static func takeScreenshotModern(_ screen: NSScreen) async throws -> NSImage? { - DispatchQueue.main.async { - // New method needs screen capture permits - ScreenCaptureManager.requestAccess() + // Second fallback: try to get any wallpaper window from the Dock, regardless of screen dimensions + if let anyWallpaperImage = try? await captureWallpaperFromDock(screenFrame: screenFrame, matchFrame: false) { + return anyWallpaperImage } - // Get content that is currently available for capture. - let availableContent = try await SCShareableContent.current - - // Find potential wallpaper windows on the screen. - let wallpaperWindows = availableContent.windows - .filter(\.isOnScreen) - .sorted(by: { $0.windowLayer < $1.windowLayer }) - .filter { ($0.title ?? "").contains("Wallpaper") } - .filter { $0.owningApplication?.bundleIdentifier == "com.apple.dock" } - - // Create a content filter to capture the wallpaper windows on the screen. - let filter = SCContentFilter( - display: availableContent.displays.first(where: { $0.displayID == screen.displayID })!, - including: wallpaperWindows - ) - let config = if #available(macOS 15.0, *) { // capture HDR on macOS 15 and later - SCStreamConfiguration(preset: .captureHDRScreenshotLocalDisplay) - } else { - SCStreamConfiguration() + // Final fallback: capture the full screen if we couldn't get any wallpaper window + if let fallbackImage = try? await captureFullScreen() { + return fallbackImage } - config.showsCursor = false - // Call the screenshot API to get CGImage representation. - let screenshotImage = try await SCScreenshotManager.captureImage( - contentFilter: filter, - configuration: config - ) + NSLog("Error: \(WallpaperProcessorError.screenshotFailed)") + return nil + } + + /// Attempts to capture the wallpaper window from the Dock app. + /// - Parameters: + /// - screenFrame: The frame of the screen to capture. + /// - matchFrame: Whether to match the exact screen frame dimensions or get any wallpaper window. + /// - Returns: An NSImage of the wallpaper or nil if the operation fails. + /// + /// This approach uses window capturing APIs to specifically target the Dock's wallpaper window. + /// It requires appropriate permissions, but provides the cleanest capture of just the wallpaper. + /// The method identifies the wallpaper window by filtering window properties from the Dock process. + private static func captureWallpaperFromDock(screenFrame: CGRect, matchFrame: Bool) async throws -> NSImage? { + // Get all windows and filter for the Dock's wallpaper windows + let windows = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as! [[CFString: Any]] + var wallpaperWindows = windows + .filter { $0[kCGWindowOwnerName] as? String == "Dock" } + .filter { ($0[kCGWindowName] as? String ?? "").contains("Wallpaper") } + .filter { $0[kCGWindowIsOnscreen] as? Int == 1 } + + // Apply additional frame filtering only if matchFrame is true + if matchFrame { + wallpaperWindows = wallpaperWindows.filter { window in + if let bounds = window[kCGWindowBounds] as? [String: CGFloat], + bounds["X"] == screenFrame.origin.x, + bounds["Y"] == screenFrame.origin.y, + bounds["Width"] == screenFrame.width, + bounds["Height"] == screenFrame.height { + true + } else { + false + } + } + } + + let windowIDs = wallpaperWindows.map { $0[kCGWindowNumber] as! CGWindowID } + + guard !windowIDs.isEmpty else { + throw WallpaperProcessorError.noWallpaperWindowsFound + } - // Convert CGImage to NSImage. - let image = NSImage( - cgImage: screenshotImage, - size: .init(width: screenshotImage.width, height: screenshotImage.height) - ) + // Use the private CGSHWCaptureWindowList API to capture high-quality images of the windows + // This approach provides better results than the public APIs for this specific use case + var captureWindowIDs = windowIDs + let cid = CGSMainConnectionID() + let images = CGSHWCaptureWindowList( + cid, + &captureWindowIDs, + captureWindowIDs.count, + [.ignoreGlobalClipShape, .bestResolution, .fullSize] + ).takeUnretainedValue() as! [CGImage] + + guard let image = images.first else { + throw WallpaperProcessorError.wallpaperWindowCaptureFailed + } - return image + return NSImage(cgImage: image, size: NSSize.zero) } - private static func takeScreenshotOld(_ screen: NSScreen) -> NSImage? { - guard let screenshotCGImage = CGDisplayCreateImage(screen.displayID!) else { - NSLog("Failed to capture the desktop wallpaper using the existing method.") - return nil + /// Fallback method to capture the entire screen. + /// This may include desktop icons and menubar, but it's better than nothing. + /// - Returns: An NSImage of the screen or nil if the operation fails. + /// + /// This method uses the public CGWindowListCreateImage API to capture what's visible on screen. + /// While this will include desktop icons and potentially other UI elements, it's a reliable + /// fallback when we can't access the wallpaper window directly, and still provides + /// useful color information in most cases. + private static func captureFullScreen() async throws -> NSImage? { + let screen = NSScreen.screenWithMouse ?? NSScreen.main ?? NSScreen.screens[0] + let rect = screen.frame + + guard let cgImage = CGWindowListCreateImage( + rect, + .optionOnScreenBelowWindow, + kCGNullWindowID, + [.shouldBeOpaque, .bestResolution] + ) else { + throw WallpaperProcessorError.screenCaptureFailed } - return NSImage(cgImage: screenshotCGImage, size: NSSize.zero) + + return NSImage(cgImage: cgImage, size: NSSize.zero) } } + +// MARK: - Private APIs + +/// Thanks to AltTab for some of the code! +/// https://github.com/lwouis/alt-tab-macos/blob/master/src/api-wrappers/private-apis/SkyLight.framework.swift + +typealias CGSConnectionID = UInt32 + +@_silgen_name("CGSMainConnectionID") +func CGSMainConnectionID() -> CGSConnectionID + +@_silgen_name("CGSHWCaptureWindowList") +func CGSHWCaptureWindowList( + _ cid: CGSConnectionID, + _ windowList: UnsafeMutablePointer, + _ windowCount: Int, + _ options: CGSWindowCaptureOptions +) -> Unmanaged + +struct CGSWindowCaptureOptions: OptionSet { + let rawValue: UInt32 + static let ignoreGlobalClipShape = CGSWindowCaptureOptions(rawValue: 1 << 11) + // on a retina display, 1px is spread on 4px, so nominalResolution is 1/4 of bestResolution + static let nominalResolution = CGSWindowCaptureOptions(rawValue: 1 << 9) + static let bestResolution = CGSWindowCaptureOptions(rawValue: 1 << 8) + // when Stage Manager is enabled, screenshots can become skewed. This param gets us full-size screenshots regardless + static let fullSize = CGSWindowCaptureOptions(rawValue: 1 << 19) +} diff --git a/Loop/Managers/PermissionsManager.swift b/Loop/Managers/AccessibilityManager.swift similarity index 54% rename from Loop/Managers/PermissionsManager.swift rename to Loop/Managers/AccessibilityManager.swift index 93f7902d..b3a1bb9a 100644 --- a/Loop/Managers/PermissionsManager.swift +++ b/Loop/Managers/AccessibilityManager.swift @@ -1,5 +1,5 @@ // -// PermissionsManager.swift +// AccessibilityManager.swift // Loop // // Created by Kai Azim on 2023-04-08. @@ -49,40 +49,3 @@ class AccessibilityManager { _ = try? Process.run(URL(filePath: "/usr/bin/tccutil"), arguments: ["reset", "Accessibility", Bundle.main.bundleID]) } } - -class ScreenCaptureManager { - static func getStatus() -> Bool { - CGPreflightScreenCaptureAccess() - } - - @discardableResult - static func requestAccess() -> Bool { - if ScreenCaptureManager.getStatus() { - return true - } - - resetScreenCapturePermissions() // Reset permissions if needed - - let alert = NSAlert() - alert.messageText = .init( - localized: .init( - "Screen Capture Request: Title", - defaultValue: "\(Bundle.main.appName) Needs Screen Recording Permissions" - ) - ) - alert.informativeText = .init( - localized: .init( - "Screen Capture Request: Content", - defaultValue: "Please grant access to be able to dynamically set Loop's accent color based on your wallpaper. Please relaunch \(Bundle.main.appName) after granting access." - ) - ) - alert.runModal() - - let accessGranted = CGRequestScreenCaptureAccess() - return accessGranted - } - - private static func resetScreenCapturePermissions() { - _ = try? Process.run(URL(filePath: "/usr/bin/tccutil"), arguments: ["reset", "ScreenCapture", Bundle.main.bundleID]) - } -} diff --git a/Loop/Radial Menu/RadialMenuController.swift b/Loop/Radial Menu/RadialMenuController.swift index 37a9358d..f57ab2b4 100644 --- a/Loop/Radial Menu/RadialMenuController.swift +++ b/Loop/Radial Menu/RadialMenuController.swift @@ -40,12 +40,27 @@ class RadialMenuController { ) ) panel.alphaValue = 0 - panel.setFrameOrigin( - NSPoint( - x: mouseX - windowSize / 2, - y: mouseY - windowSize / 2 + + // Position the panel + if Defaults[.lockRadialMenuToCenter], let screen = NSApp.keyWindow?.screen ?? NSScreen.main { + // Position at the center of the screen + let screenFrame = screen.frame + panel.setFrameOrigin( + NSPoint( + x: screenFrame.midX - windowSize / 2, + y: screenFrame.midY - windowSize / 2 + ) ) - ) + } else { + // Position at the mouse cursor + panel.setFrameOrigin( + NSPoint( + x: mouseX - windowSize / 2, + y: mouseY - windowSize / 2 + ) + ) + } + panel.orderFrontRegardless() controller = .init(window: panel) diff --git a/Loop/Utilities/Migrator.swift b/Loop/Utilities/Migrator.swift index 94f8b652..08a98638 100644 --- a/Loop/Utilities/Migrator.swift +++ b/Loop/Utilities/Migrator.swift @@ -187,9 +187,89 @@ private extension Migrator { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - let data = try encoder.encode(keybinds) - guard let json = String(data: data, encoding: .utf8) else { + // Convert to a dictionary we can manipulate before final encoding + var rootDict = try JSONSerialization.jsonObject( + with: encoder.encode(keybinds), + options: [.mutableContainers] + ) as! [String: Any] + + // Process trigger key if present + if var triggerKey = rootDict["triggerKey"] as? [CGKeyCode] { + triggerKey.sort() + rootDict["triggerKey"] = triggerKey + } + + // Process actions array + if var actions = rootDict["actions"] as? [[String: Any]] { + // First ensure all keybind arrays are sorted + for i in 0 ..< actions.count { + // Sort the keybind array in each action + if var keybind = actions[i]["keybind"] as? [CGKeyCode] { + keybind.sort() + actions[i]["keybind"] = keybind + } + + // Handle nested cycle actions if present + if var cycle = actions[i]["cycle"] as? [[String: Any]] { + // Sort the cycle actions by direction + cycle.sort { first, second -> Bool in + let firstDir = first["direction"] as? String ?? "" + let secondDir = second["direction"] as? String ?? "" + return firstDir < secondDir + } + + // For each action in the cycle + for j in 0 ..< cycle.count { + // Sort the keybind array in each cycle action + if var cycleKeybind = cycle[j]["keybind"] as? [CGKeyCode] { + cycleKeybind.sort() + cycle[j]["keybind"] = cycleKeybind + } + } + actions[i]["cycle"] = cycle + } + } + + // Sort the actions array by direction and keybind for a consistent ordering + actions.sort { first, second -> Bool in + // First compare by direction + let firstDir = first["direction"] as? String ?? "" + let secondDir = second["direction"] as? String ?? "" + + if firstDir != secondDir { + return firstDir < secondDir + } + + // If directions are equal, compare by name (if present) + let firstName = first["name"] as? String ?? "" + let secondName = second["name"] as? String ?? "" + + if firstName != secondName { + return firstName < secondName + } + + // If names are equal or empty, compare by keybind + let firstKeybind = first["keybind"] as? [CGKeyCode] ?? [] + let secondKeybind = second["keybind"] as? [CGKeyCode] ?? [] + + // Convert keybinds to strings for comparison + let firstKeyStr = firstKeybind.map { String($0) }.joined(separator: "-") + let secondKeyStr = secondKeybind.map { String($0) }.joined(separator: "-") + + return firstKeyStr < secondKeyStr + } + + rootDict["actions"] = actions + } + + // Convert back to JSON data with our sorted arrays + let sortedData = try JSONSerialization.data( + withJSONObject: rootDict, + options: [.prettyPrinted, .sortedKeys] + ) + + guard let json = String(data: sortedData, encoding: .utf8) else { throw MigratorError.failedToConvertToString }