diff --git a/MacPacker/Features/Settings/GeneralSettingsView.swift b/MacPacker/Features/Settings/GeneralSettingsView.swift index f5c96a9..7e96c4e 100644 --- a/MacPacker/Features/Settings/GeneralSettingsView.swift +++ b/MacPacker/Features/Settings/GeneralSettingsView.swift @@ -21,6 +21,9 @@ struct GeneralSettingsView: View { @AppStorage(Keys.rememberRecentArchives) var rememberRecentArchives: Bool = true @AppStorage(Keys.smartExtraction, store: .macPackerShared) var smartExtraction: Bool = true + @AppStorage(Keys.excludeMacMetadata) var excludeMacMetadata = false + @AppStorage(Keys.requireWindowsCompatibleNames) var requireWindowsCompatibleNames = false + var body: some View { VStack(spacing: 8) { HStack(alignment: .top) { @@ -119,6 +122,27 @@ struct GeneralSettingsView: View { .padding(.leading, 8) .frame(width: 240, alignment: .leading) } + HStack(alignment: .top) { + Text("Exclude Mac-specific metadata", comment: "Global compression setting label") + .frame(width: 200, alignment: .trailing) + VStack(alignment: .leading, spacing: 4) { + Toggle(isOn: $excludeMacMetadata) { EmptyView() } + .labelsHidden() + .accessibilityIdentifier("settings.excludeMacMetadata") + Toggle(isOn: $requireWindowsCompatibleNames) { + Text("Windows-compatible filenames", comment: "Optional filename validation under the metadata exclusion setting") + } + .accessibilityIdentifier("settings.windowsCompatibleNames") + .help(Text("Stops before saving and lists incompatible or conflicting filenames. Files are never renamed.", comment: "Help for the Windows-compatible filename validation setting")) + .disabled(!excludeMacMetadata) + Text("Leaves out .DS_Store, resource forks and Finder metadata when compressing or saving. Hidden flags and custom folder icons are not preserved.", comment: "Consequences of excluding Mac metadata from archives") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + .padding(.leading, 8) + .frame(width: 240, alignment: .leading) + } } .padding() // Turning it off is a privacy switch, so what was collected goes with it — diff --git a/MacPacker/Localizable.xcstrings b/MacPacker/Localizable.xcstrings index 453bc36..9cea856 100644 --- a/MacPacker/Localizable.xcstrings +++ b/MacPacker/Localizable.xcstrings @@ -1,6 +1,20 @@ { "sourceLanguage": "en", "strings": { + "Exclude Mac-specific metadata": { + "comment": "Global compression setting label", + "extractionState": "manual" + }, + "Leaves out .DS_Store, resource forks and Finder metadata when compressing or saving. Hidden flags and custom folder icons are not preserved.": { + "comment": "Consequences of excluding Mac metadata from archives", + "extractionState": "manual" + }, + "Stops before saving and lists incompatible or conflicting filenames. Files are never renamed.": { + "extractionState": "manual" + }, + "Windows-compatible filenames": { + "extractionState": "manual" + }, "appExtractionInProgress": { "comment": "Title of the alert shown when the user quits while an extraction is running.", "extractionState": "manual", @@ -17,7 +31,7 @@ "value": "An extraction is still in progress" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Una extracción sigue en proceso" @@ -65,7 +79,7 @@ "value": "Rozpakowywanie nadal jest w toku" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "An extraction is still in progress" @@ -125,7 +139,7 @@ "value": "Quit Anyway" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Salir" @@ -173,7 +187,7 @@ "value": "Zakończ mimo to" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Encerrar Mesmo Assim" @@ -233,7 +247,7 @@ "value": "Quitting now stops the extraction and may leave incomplete files at the destination." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Salir ahora detendrá la extracción y podría dejar archivos incompletos en el destino." @@ -281,7 +295,7 @@ "value": "Zakończenie operacji rozpakowywania może spowodować niekompletność plików w miejscu docelowym." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Quitting now stops the extraction and may leave incomplete files at the destination." @@ -340,7 +354,7 @@ "value": "Delete the selected items from the archive" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Eliminar los elementos seleccionados del archivo comprimido" @@ -388,7 +402,7 @@ "value": "Usuń zaznaczone obiekty z archiwum" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Delete the selected items from the archive" @@ -448,7 +462,7 @@ "value": "Modified" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Modificación" @@ -496,7 +510,7 @@ "value": "Zmodyfikowano" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Modificado" @@ -556,7 +570,7 @@ "value": "Enclosing Folder" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Carpeta contenedora" @@ -604,7 +618,7 @@ "value": "Folder obejmujący" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Pasta Original" @@ -664,7 +678,7 @@ "value": "Open in New Window" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Abrir en una ventana nueva" @@ -712,7 +726,7 @@ "value": "Otwórz w nowym oknie" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Abrir em Nova Janela" @@ -772,7 +786,7 @@ "value": "Quick Look" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Vista rápida" @@ -820,7 +834,7 @@ "value": "Szybki przegląd" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Visualização Rápida" @@ -880,7 +894,7 @@ "value": "Drop here to compress" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Suelta aquí para comprimir" @@ -928,7 +942,7 @@ "value": "Upuść tutaj, aby zarchiwizować." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Solte aqui para comprimir" @@ -988,7 +1002,7 @@ "value": "Compress here" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Comprime aquí" @@ -1036,7 +1050,7 @@ "value": "Archiwizuj tutaj" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Comprimir Aqui" @@ -1096,7 +1110,7 @@ "value": "Keep on Top" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Mantener arriba" @@ -1144,7 +1158,7 @@ "value": "Pozostaw na górze" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Manter em Primeiro Plano" @@ -1203,7 +1217,7 @@ "value": "Add files or folders to the archive" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Añadir archivos o carpetas al archivo comprimido" @@ -1251,7 +1265,7 @@ "value": "Dodaj pliki lub foldery do archiwum" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Add files or folders to the archive" @@ -1311,7 +1325,7 @@ "value": "Archive info" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Información del archivo" @@ -1359,7 +1373,7 @@ "value": "Informacje o Archiwum" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Informações de Arquivo" @@ -1419,7 +1433,7 @@ "value": "Automatic" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Automáticamente" @@ -1467,7 +1481,7 @@ "value": "Automatycznie" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Automaticamente" @@ -1527,7 +1541,7 @@ "value": "Change the password under Options…" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Cambia la contraseña en Opciones…" @@ -1575,7 +1589,7 @@ "value": "Zmień hasło używając Opcji..." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Change the password under Options…" @@ -1635,7 +1649,7 @@ "value": "A zip password can only use letters A–Z, digits, spaces and common symbols. 7z takes any password." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "La contraseña de un archivo zip sólo puede contener letras A-Z, números, espacios y símbolos comunes. Los archivos 7z pueden tener cualquier contraseña." @@ -1683,7 +1697,7 @@ "value": "Hasło ZIP może zawierać tylko znaki A-Z, cyfry, spacje i symbole ogólne. 7z akceptuje każde hasło." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "A zip password can only use letters A–Z, digits, spaces and common symbols. 7z takes any password." @@ -1743,7 +1757,7 @@ "value": "Create a new archive" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Crear un nuevo archivo comprimido" @@ -1791,7 +1805,7 @@ "value": "Utwórz nowe archiwum" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Create a new archive" @@ -1851,7 +1865,7 @@ "value": "Choose where to extract" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Selecciona a donde extraer" @@ -1899,7 +1913,7 @@ "value": "Wybierz miejsce rozpakowania" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Choose where to extract" @@ -1959,7 +1973,7 @@ "value": "%@ left" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Quedan %@" @@ -2007,7 +2021,7 @@ "value": "%@ pozostało" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "%@ left" @@ -2067,7 +2081,7 @@ "value": "With AES-256, a zip password can be at most %lld characters long." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Con AES-256, la contraseña de un zip puede tener como máximo %lld caracteres." @@ -2115,7 +2129,7 @@ "value": "Z szyfrowaniem AES-256 hasło zip może mieć do %lld znaków." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "With AES-256, a zip password can be at most %lld characters long." @@ -2175,7 +2189,7 @@ "value": "ZipCrypto is easily broken. Pick it only for tools that cannot open AES-256." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "ZipCrypto falla regularmente. Úsalo solo para herramientas que no puedan abrir archivos cifrados con AES-256." @@ -2223,7 +2237,7 @@ "value": "ZipCrypto można łatwo złamać. Używaj tylko tam, gdzie nie można użyć AES-256." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "ZipCrypto is easily broken. Pick it only for tools that cannot open AES-256." @@ -2283,7 +2297,7 @@ "value": "the archive" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "el archivo comprimido" @@ -2331,7 +2345,7 @@ "value": "archiwum" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "the archive" @@ -2391,7 +2405,7 @@ "value": "Dictionary size" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Tamaño del diccionario" @@ -2439,7 +2453,7 @@ "value": "Rozmiar słownika" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Tamanho do dicionário" @@ -2499,7 +2513,7 @@ "value": "Don't Save" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "No guardar" @@ -2547,7 +2561,7 @@ "value": "Nie zachowuj" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Não Salvar" @@ -2607,7 +2621,7 @@ "value": "Don't split" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "No dividir" @@ -2655,7 +2669,7 @@ "value": "Nie rozdzielaj" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Don't split" @@ -2715,7 +2729,7 @@ "value": "Encrypt file names" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Cifrar nombres de archivo" @@ -2763,7 +2777,7 @@ "value": "Zaszyfruj nazwy plików" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Encriptar nomes de ficheiros" @@ -2823,7 +2837,7 @@ "value": "File names stay readable in a zip archive. 7z can encrypt them too." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "En archivos zip los nombres de los archivos siguen siendo visibles. En archivos 7z pueden ser cifrados." @@ -2871,7 +2885,7 @@ "value": "Nazwy plików będą odczytywalne w archiwum ZIP. 7z potrafi je szyfrować." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "File names stay readable in a zip archive. 7z can encrypt them too." @@ -2931,7 +2945,7 @@ "value": "Encryption method" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Método de cifrado" @@ -2979,7 +2993,7 @@ "value": "Sposób szyfrowania" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Método de encriptação" @@ -3039,7 +3053,7 @@ "value": "Exclude .DS_Store files" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Excluir archivos .DS_Store" @@ -3087,7 +3101,7 @@ "value": "Wyklucz pliki .DS_Store" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Exclude .DS_Store files" @@ -3147,7 +3161,7 @@ "value": "Format" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Formato" @@ -3195,7 +3209,7 @@ "value": "Format" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Formato" @@ -3255,7 +3269,7 @@ "value": "Fast" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Rápida" @@ -3303,7 +3317,7 @@ "value": "Szybko" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Rápida" @@ -3363,7 +3377,7 @@ "value": "Fastest" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "El más rápido" @@ -3411,7 +3425,7 @@ "value": "Najszybsza" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Muito rápido" @@ -3471,7 +3485,7 @@ "value": "Maximum" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Máxima" @@ -3519,7 +3533,7 @@ "value": "Maksymalna" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Máxima" @@ -3579,7 +3593,7 @@ "value": "Normal" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Normal" @@ -3627,7 +3641,7 @@ "value": "Normalna" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Normal" @@ -3687,7 +3701,7 @@ "value": "Store" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Sin compresión" @@ -3735,7 +3749,7 @@ "value": "Bez kompresji" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Sem compressão" @@ -3795,7 +3809,7 @@ "value": "Ultra" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Ultra" @@ -3843,7 +3857,7 @@ "value": "Ultra" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Ultra" @@ -3903,7 +3917,7 @@ "value": "Method" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Método" @@ -3951,7 +3965,7 @@ "value": "Tryb" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Método" @@ -4011,7 +4025,7 @@ "value": "Non-solid" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "No sólido" @@ -4059,7 +4073,7 @@ "value": "Nieciągły" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Não sólido" @@ -4119,7 +4133,7 @@ "value": "Repeat" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Repetir" @@ -4167,7 +4181,7 @@ "value": "Powtórz" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Repetir" @@ -4227,7 +4241,7 @@ "value": "Solid" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Sólido" @@ -4275,7 +4289,7 @@ "value": "Ciągły" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Sólido" @@ -4335,7 +4349,7 @@ "value": "Solid block size" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Tamaño de bloque sólido" @@ -4383,7 +4397,7 @@ "value": "Rozmiar bloku ciągłego" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Tamanho dos blocos sólidos" @@ -4443,7 +4457,7 @@ "value": "Split into volumes" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Dividir en volúmenes" @@ -4491,7 +4505,7 @@ "value": "Dziel na woluminy" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Split into volumes" @@ -4551,7 +4565,7 @@ "value": "Do you want to save the changes you made to “%@”?" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Deseas guardar los cambios que hiciste a “%@”?" @@ -4599,7 +4613,7 @@ "value": "Czy chcesz zapisać zmiany, które wprowadziłeś do “%@”?" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Do you want to save the changes you made to “%@”?" @@ -4659,7 +4673,7 @@ "value": "Your changes will be lost if you don’t save them." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Tus cambios se perderán si no los guardas." @@ -4707,7 +4721,7 @@ "value": "Twoje zmiany zostaną utracone, jeśli ich nie zapiszesz." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Your changes will be lost if you don’t save them." @@ -4767,7 +4781,7 @@ "value": "Word size" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Tamaño de la palabra" @@ -4815,7 +4829,7 @@ "value": "Rozmiar słowa" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Tamanho da palavra" @@ -4875,7 +4889,7 @@ "value": "Date Modified" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Fecha de modificación" @@ -4923,7 +4937,7 @@ "value": "Data modyfikacji" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Data de Modificação" @@ -4983,7 +4997,7 @@ "value": "Name" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Nombre" @@ -5031,7 +5045,7 @@ "value": "Nazwa" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Nome" @@ -5091,7 +5105,7 @@ "value": "Packed Size" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Tamaño comprimido" @@ -5139,7 +5153,7 @@ "value": "Rozmiar po spakowaniu" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Tamanho total" @@ -5199,7 +5213,7 @@ "value": "Permissions" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Permisos" @@ -5247,7 +5261,7 @@ "value": "Uprawnienia" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Permissões" @@ -5307,7 +5321,7 @@ "value": "Size" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Tamaño" @@ -5355,7 +5369,7 @@ "value": "Rozmiar" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Tamanho" @@ -5415,7 +5429,7 @@ "value": "About %@" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Acerca de %@" @@ -5463,7 +5477,7 @@ "value": "O %@" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Sobre %@" @@ -5523,7 +5537,7 @@ "value": "Acknowledgements" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Reconocimientos" @@ -5571,7 +5585,7 @@ "value": "Podziękowania" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Reconhecimentos" @@ -5631,7 +5645,7 @@ "value": "Add" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Agregar" @@ -5679,7 +5693,7 @@ "value": "Dodaj" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Adicionar" @@ -5739,7 +5753,7 @@ "value": "Add to archive..." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Agregar al archivo comprimido..." @@ -5787,7 +5801,7 @@ "value": "Dodaj do archiwum..." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Adicionar ao arquivo..." @@ -5847,7 +5861,7 @@ "value": "Add to “%@”" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Agregar a “%@”" @@ -5895,7 +5909,7 @@ "value": "Dodaj do “%@”" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Adicionar a “%@”" @@ -5955,7 +5969,7 @@ "value": "Back" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Atrás" @@ -6003,7 +6017,7 @@ "value": "Wróć" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Voltar" @@ -6063,7 +6077,7 @@ "value": "Bottom" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Abajo" @@ -6111,7 +6125,7 @@ "value": "na dole" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Inferior" @@ -6171,7 +6185,7 @@ "value": "Cancel" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Cancelar" @@ -6219,7 +6233,7 @@ "value": "Anuluj" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Cancel" @@ -6279,7 +6293,7 @@ "value": "Clear" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Borrar" @@ -6327,7 +6341,7 @@ "value": "Wyczyść" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Clear" @@ -6386,7 +6400,7 @@ "value": "Compress Contents of \"%@\"" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Comprimir el contenido de \"%@\"" @@ -6434,7 +6448,7 @@ "value": "Spakuj zawartość \"%@\"" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Compress Contents of \"%@\"" @@ -6493,7 +6507,7 @@ "value": "Compress to \"%@\"" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Comprimir en \"%@\"" @@ -6541,7 +6555,7 @@ "value": "Spakuj do \"%@\"" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Compress to \"%@\"" @@ -6601,7 +6615,7 @@ "value": "Compression" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Compresión" @@ -6649,7 +6663,7 @@ "value": "Archiwizacja" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Compressão" @@ -6709,7 +6723,7 @@ "value": "Continue" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Continuar" @@ -6757,7 +6771,7 @@ "value": "Dalej" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Continuar" @@ -6817,7 +6831,7 @@ "value": "Delete" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Eliminar" @@ -6865,7 +6879,7 @@ "value": "Usuń" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Apagar" @@ -6925,7 +6939,7 @@ "value": "Disabled" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Deshabilitado" @@ -6973,7 +6987,7 @@ "value": "Wyłączone" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Desativado" @@ -7033,7 +7047,7 @@ "value": "Done" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Listo" @@ -7081,7 +7095,7 @@ "value": "OK" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "OK" @@ -7141,7 +7155,7 @@ "value": "enabled" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Habilitado" @@ -7189,7 +7203,7 @@ "value": "Włączone" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Ativado" @@ -7249,7 +7263,7 @@ "value": "Extract" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Extraer" @@ -7297,7 +7311,7 @@ "value": "Wyodrębnij" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Extrair" @@ -7357,7 +7371,7 @@ "value": "Extract archive" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Extraer archivo" @@ -7405,7 +7419,7 @@ "value": "Rozpakuj archiwum" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Extrair Arquivo" @@ -7465,7 +7479,7 @@ "value": "Extract Here" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Extraer aquí" @@ -7513,7 +7527,7 @@ "value": "Wypakuj tutaj" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Extrair para aqui" @@ -7573,7 +7587,7 @@ "value": "Extract selected" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Extraer selección" @@ -7621,7 +7635,7 @@ "value": "Rozpakuj zaznaczone" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Extrair seleção" @@ -7680,7 +7694,7 @@ "value": "Extract to \"%@\"" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Extraer en \"%@\"" @@ -7728,7 +7742,7 @@ "value": "Rozpakuj do \"%@\"" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Extract to \"%@\"" @@ -7788,7 +7802,7 @@ "value": "Extracting" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Extrayendo" @@ -7836,7 +7850,7 @@ "value": "Wypakowanie" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Extracting" @@ -7896,7 +7910,7 @@ "value": "Find" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Buscar" @@ -7944,7 +7958,7 @@ "value": "Znajdź" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Buscar" @@ -8004,7 +8018,7 @@ "value": "Grant Access" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Permitir acceso" @@ -8052,7 +8066,7 @@ "value": "Udziel dostępu" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Grant Access" @@ -8112,7 +8126,7 @@ "value": "Help with translation" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Ayuda a traducir" @@ -8160,7 +8174,7 @@ "value": "Pomoc w tłumaczeniu" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Help with translation" @@ -8220,7 +8234,7 @@ "value": "Legal Notice" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Aviso legal" @@ -8268,7 +8282,7 @@ "value": "Nota prawna" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Aviso legal" @@ -8352,7 +8366,7 @@ } } }, - "es-mx": { + "es-MX": { "variations": { "plural": { "many": { @@ -8514,7 +8528,7 @@ } } }, - "pt-br": { + "pt-BR": { "variations": { "plural": { "many": { @@ -8670,7 +8684,7 @@ "value": "Learn more…" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Obtén detalles…" @@ -8718,7 +8732,7 @@ "value": "Więcej informacji…" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Saiba mais…" @@ -8778,7 +8792,7 @@ "value": "More" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Más" @@ -8826,7 +8840,7 @@ "value": "Więcej" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Mais" @@ -8886,7 +8900,7 @@ "value": "New Archive" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Crear archivo comprimido" @@ -8934,7 +8948,7 @@ "value": "Nowe archiwum" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "New Archive" @@ -8994,7 +9008,7 @@ "value": "New %@ Window" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Nueva ventana de %@" @@ -9042,7 +9056,7 @@ "value": "Nowe %@ Okno" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "New %@ Window" @@ -9102,7 +9116,7 @@ "value": "None" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Ninguno" @@ -9150,7 +9164,7 @@ "value": "Brak" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Nenhuma" @@ -9209,7 +9223,7 @@ "value": "Nothing selected" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Nada seleccionado" @@ -9257,7 +9271,7 @@ "value": "Nic nie jest zaznaczone" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Não há nada selecionado" @@ -9317,7 +9331,7 @@ "value": "OK" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Aceptar" @@ -9365,7 +9379,7 @@ "value": "OK" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "OK" @@ -9425,7 +9439,7 @@ "value": "Open" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Abrir" @@ -9473,7 +9487,7 @@ "value": "Otwórz z dysku" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Abrir" @@ -9533,7 +9547,7 @@ "value": "Open Archive" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Abrir archivo comprimido" @@ -9581,7 +9595,7 @@ "value": "Otwórz archiwum" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Abrir arquivo" @@ -9640,7 +9654,7 @@ "value": "Open %lld Archives" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Abrir %lld archivos comprimidos" @@ -9688,7 +9702,7 @@ "value": "Otwórz %lld archiwa" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Open %lld Archives" @@ -9748,7 +9762,7 @@ "value": "Options" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Opciones" @@ -9796,7 +9810,7 @@ "value": "Opcje" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Opções" @@ -9856,7 +9870,7 @@ "value": "Password:" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Contraseña:" @@ -9904,7 +9918,7 @@ "value": "Hasło:" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Senha:" @@ -9964,7 +9978,7 @@ "value": "“%@” is password protected." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "“%@” está protegido con contraseña." @@ -10012,7 +10026,7 @@ "value": "„%@” jest chroniony hasłem." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "“%@” está protegido por senha." @@ -10072,7 +10086,7 @@ "value": "Preview" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Vista previa" @@ -10120,7 +10134,7 @@ "value": "Podgląd" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Pré Visualização" @@ -10180,7 +10194,7 @@ "value": "Privacy" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Privacidad" @@ -10228,7 +10242,7 @@ "value": "Prywatność" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Privacidade" @@ -10288,7 +10302,7 @@ "value": "Quick Compress" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Compresión rápida" @@ -10336,7 +10350,7 @@ "value": "Szybka kompresja" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Quick Compress" @@ -10396,7 +10410,7 @@ "value": "Quick Compress Window" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Ventana de Compresión rápida" @@ -10444,7 +10458,7 @@ "value": "Okno szybkiej kompresji" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Quick Compress Window" @@ -10504,7 +10518,7 @@ "value": "Refresh" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Actualizar" @@ -10552,7 +10566,7 @@ "value": "Odśwież" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Recarregar" @@ -10612,7 +10626,7 @@ "value": "save" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "guardar" @@ -10660,7 +10674,7 @@ "value": "Zachowaj" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "salvar" @@ -10720,7 +10734,7 @@ "value": "Save As…" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Guardar como…" @@ -10768,7 +10782,7 @@ "value": "Zachowaj jako…" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Salvar Como…" @@ -10852,7 +10866,7 @@ } } }, - "es-mx": { + "es-MX": { "variations": { "plural": { "many": { @@ -11014,7 +11028,7 @@ } } }, - "pt-br": { + "pt-BR": { "variations": { "plural": { "many": { @@ -11170,7 +11184,7 @@ "value": "Settings..." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Configuración..." @@ -11218,7 +11232,7 @@ "value": "Ustawienia…" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Configurações..." @@ -11278,7 +11292,7 @@ "value": "Start" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Iniciar" @@ -11326,7 +11340,7 @@ "value": "Start" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Iniciar" @@ -11386,7 +11400,7 @@ "value": "Terms & Conditions" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Términos y condiciones" @@ -11434,7 +11448,7 @@ "value": "Warunki i zasady" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Termos e Condições" @@ -11494,7 +11508,7 @@ "value": "Top" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Arriba" @@ -11542,7 +11556,7 @@ "value": "Na górze" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Acima" @@ -11602,7 +11616,7 @@ "value": "Website" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Sitio web" @@ -11650,7 +11664,7 @@ "value": "Strona Internetowa" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Site" @@ -11710,7 +11724,7 @@ "value": "What's new" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Novedades" @@ -11758,7 +11772,7 @@ "value": "Co nowego" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Novidades" @@ -11818,7 +11832,7 @@ "value": "Contact us" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Contáctanos" @@ -11866,7 +11880,7 @@ "value": "Skontaktuj się z nami" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Fale Conosco" @@ -11926,7 +11940,7 @@ "value": "... raise a Bug" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "...reporta un Bug" @@ -11974,7 +11988,7 @@ "value": "… zgłoś błąd" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Reportar falhas" @@ -12034,7 +12048,7 @@ "value": "... send a mail to %@" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "...envía un correo a %@" @@ -12082,7 +12096,7 @@ "value": "… wyślij wiadomość do %@" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Escrever para %@" @@ -12142,7 +12156,7 @@ "value": "... request a Feature" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "...solicita funcionalidad" @@ -12190,7 +12204,7 @@ "value": "… wyślij propozycje" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Pedir nova função" @@ -12250,7 +12264,7 @@ "value": "Go here to ..." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Más opciones..." @@ -12298,7 +12312,7 @@ "value": "Przejdź do …" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Clique aqui para" @@ -12358,7 +12372,7 @@ "value": "Export failed" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Exportación fallida" @@ -12406,7 +12420,7 @@ "value": "Eksport nie powiódł się" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Export failed" @@ -12466,7 +12480,7 @@ "value": "Could not open archive" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "No se ha podido abrir el archivo comprimido" @@ -12514,7 +12528,7 @@ "value": "Nie można otworzyć archiwum" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Could not open archive" @@ -12574,7 +12588,7 @@ "value": "That password is incorrect. Try again." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "La contraseña es incorrecta. Inténtalo nuevamente." @@ -12622,7 +12636,7 @@ "value": "To hasło jest nieprawidłowe. Spróbuj ponownie." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "That password is incorrect. Try again." @@ -12682,7 +12696,7 @@ "value": "Passwords do not match" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Las contraseñas no coinciden" @@ -12730,7 +12744,7 @@ "value": "Hasła nie zgadzają się" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "As palavras-passe não coincidem" @@ -12790,7 +12804,7 @@ "value": "Could not save archive" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "No se pudo guardar el archivo" @@ -12838,7 +12852,7 @@ "value": "Nie udało się zapisać archiwum" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Could not save archive" @@ -12898,7 +12912,7 @@ "value": "Cancelled - no access to that folder" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Cancelado - sin acceso a ese folder" @@ -12946,7 +12960,7 @@ "value": "Anulowano – brak dostępu do tego folderu" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Cancelled - no access to that folder" @@ -13006,7 +13020,7 @@ "value": "Leave a review in the App Store" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Escribe una reseña en la App Store" @@ -13054,7 +13068,7 @@ "value": "Zostaw opinie w App Store" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Deixe uma avaliação na App Store" @@ -13114,7 +13128,7 @@ "value": "Follow" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Seguir" @@ -13162,7 +13176,7 @@ "value": "Obserwuj" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Follow" @@ -13222,7 +13236,7 @@ "value": "Review" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Reseña" @@ -13270,7 +13284,7 @@ "value": "Ocena" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Review" @@ -13330,7 +13344,7 @@ "value": "Send a smile" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Enviar una sonrisa" @@ -13378,7 +13392,7 @@ "value": "Zostaw opinie" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Enviar um sorriso" @@ -13438,7 +13452,7 @@ "value": "Star" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Estrella" @@ -13486,7 +13500,7 @@ "value": "Polub" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Star" @@ -13546,7 +13560,7 @@ "value": "Star the repository on GitHub" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Añadir una estrella en GitHub" @@ -13594,7 +13608,7 @@ "value": "Dodaj do ulubionych na GitHubie" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Avalie o repositório no GitHub" @@ -13654,7 +13668,7 @@ "value": "Support the development..." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Apoya el desarrollo..." @@ -13702,7 +13716,7 @@ "value": "Wesprzyj rozwój aplikacji…" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Suporte o desenvolvimento" @@ -13762,7 +13776,7 @@ "value": "Translate" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Traducir" @@ -13810,7 +13824,7 @@ "value": "Tłumaczenie" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Traduzir" @@ -13870,7 +13884,7 @@ "value": "Guides & documentation" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Guías y documentación" @@ -13918,7 +13932,7 @@ "value": "Przewodniki i dokumentacja" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Guias e documentação" @@ -13978,7 +13992,7 @@ "value": "Learn to use %@" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Aprende a usar %@" @@ -14026,7 +14040,7 @@ "value": "Jak użyć %@" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Learn to use %@" @@ -14086,7 +14100,7 @@ "value": "No recent archives." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "No hay archivos comprimidos recientes" @@ -14134,7 +14148,7 @@ "value": "Brak ostatnich archiwów." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "No recent archives." @@ -14194,7 +14208,7 @@ "value": "Recent" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Recientes" @@ -14242,7 +14256,7 @@ "value": "Ostatnie" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Recentes" @@ -14302,7 +14316,7 @@ "value": "Release Notes" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Notas de la versión" @@ -14350,7 +14364,7 @@ "value": "Informacje o wersji" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Notas de Lançamento" @@ -14410,7 +14424,7 @@ "value": "%@ needs access to %@" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "%@ necesita acceso a %@" @@ -14458,7 +14472,7 @@ "value": "%@ wymaga dostępu do %@" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "%@ needs access to %@" @@ -14518,7 +14532,7 @@ "value": "About" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Información" @@ -14566,7 +14580,7 @@ "value": "Informacje" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Sobre" @@ -14626,7 +14640,7 @@ "value": "Access granted" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Acceso permitido" @@ -14674,7 +14688,7 @@ "value": "Dostęp nadany" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Access granted" @@ -14734,7 +14748,7 @@ "value": "Advanced" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Avanzado" @@ -14782,7 +14796,7 @@ "value": "Zaawansowane" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Avançado" @@ -14842,7 +14856,7 @@ "value": "Archive Formats" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Formatos de archivos comprimidos" @@ -14890,7 +14904,7 @@ "value": "Format Archiwum" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Formatos de Arquivos" @@ -14950,7 +14964,7 @@ "value": "Automatic engine selection" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Selección automática del motor" @@ -14998,7 +15012,7 @@ "value": "Automatyczny wybór silnika" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Automatic engine selection" @@ -15058,7 +15072,7 @@ "value": "MacPacker chooses the engine for each archive, and tries another one if the first can't read it. Turn this off to choose engines yourself." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "MacPacker selecciona el motor para abrir cada tipo de archivo comprimido e intenta con otro si el primer motor no lo puede leer. Deshabilita esta opción si deseas seleccionar tus motores manualmente." @@ -15106,7 +15120,7 @@ "value": "MacPacker wybiera silnik dla każdego archiwum i próbuje innego, jeśli pierwszy nie może go odczytać. Wyłącz tę opcję, aby samodzielnie wybierać silniki." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "MacPacker chooses the engine for each archive, and tries another one if the first can't read it. Turn this off to choose engines yourself." @@ -15166,7 +15180,7 @@ "value": "Automatically check for updates" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Comprobar actualizaciones automaticamente" @@ -15214,7 +15228,7 @@ "value": "Automatycznie szukaj aktualizacji" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Automatically check for updates" @@ -15274,7 +15288,7 @@ "value": "Breadcrumb position" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Posición de la ruta de navegación" @@ -15322,7 +15336,7 @@ "value": "Pole nawigacyjne" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Posição da barra de navegação" @@ -15382,7 +15396,7 @@ "value": "Cache" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Caché" @@ -15430,7 +15444,7 @@ "value": "Pamięć podręczna" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Cachê" @@ -15490,7 +15504,7 @@ "value": "Clear cache" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Limpiar caché" @@ -15538,7 +15552,7 @@ "value": "Wyczyść Pamięć podręczną" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Limpar Cachê" @@ -15598,7 +15612,7 @@ "value": "Clears all content from the cache" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Limpiar todo el contenido del caché" @@ -15646,7 +15660,7 @@ "value": "Wyczyść całą zawartość pamięci podręcznej" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Limpa todo o conteúdo do cachê" @@ -15706,7 +15720,7 @@ "value": "Columns" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Columnas" @@ -15754,7 +15768,7 @@ "value": "Kolumny:" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Colunas:" @@ -15814,7 +15828,7 @@ "value": "Compress each item separately" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Comprimir cada elemento por separado" @@ -15862,7 +15876,7 @@ "value": "Skompresuj każdy element osobno" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Compress each item separately" @@ -15922,7 +15936,7 @@ "value": "Compress a folder’s contents" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Comprimir el contenido de una carpeta" @@ -15970,7 +15984,7 @@ "value": "Skompresuj zwartość folderu" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Compress a folder’s contents" @@ -16030,7 +16044,7 @@ "value": "Compress to 7z" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Comprimir a 7z" @@ -16078,7 +16092,7 @@ "value": "Skompresuj do 7z" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Compress to 7z" @@ -16138,7 +16152,7 @@ "value": "Compress to zip" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Comprimir a zip" @@ -16186,7 +16200,7 @@ "value": "Skompresuj do zip" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Compress to zip" @@ -16246,7 +16260,7 @@ "value": "Compress to zip with date and time" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Comprimir a zip con fecha y hora" @@ -16294,7 +16308,7 @@ "value": "Skompresuj do zip z datą i czasem" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Compress to zip with date and time" @@ -16354,7 +16368,7 @@ "value": "Context Menu" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Menú contextual" @@ -16402,7 +16416,7 @@ "value": "Menu kontekstowe" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Menu Contextual" @@ -16462,7 +16476,7 @@ "value": "To make MacPacker the default for a file type: Right-click a file → 'Get Info' → choose MacPacker under 'Open with:' → click 'Change All…' to apply it to all similar archives." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Para hacer que MacPacker sea la aplicación por defecto para un tipo de archivo en específico: Haz clic derecho en un archivo → 'Obtener Información' → selecciona MacPacker en el menu 'Abrir con:' → Haz clic en 'Cambiar todo…' para aplicar los cambios a todos los archivos del mismo tipo." @@ -16510,7 +16524,7 @@ "value": "Aby ustawić MacPacker domyślnie dla danego typu pliku: Kliknij plik prawym przyciskiem myszy → „Informacje” → wybierz MacPacker w sekcji „Otwórz za pomocą:” → kliknij „Zmień wszystko…”, aby zastosować tę opcję do wszystkich podobnych archiwów." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "To make MacPacker the default for a file type: Right-click a file → 'Get Info' → choose MacPacker under 'Open with:' → click 'Change All…' to apply it to all similar archives." @@ -16570,7 +16584,7 @@ "value": "Default status & engine settings" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Configuración por defecto y motores de compresión" @@ -16618,7 +16632,7 @@ "value": "Ustawienia domyślne oraz ustawienia jądra" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Status Padrão & Configuração de Motor" @@ -16678,7 +16692,7 @@ "value": "Edits archives, not only opens them" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Edita archivos comprimidos, no solo los abre." @@ -16726,7 +16740,7 @@ "value": "Edytuje archiwa, nie tylko je otwiera" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Edits archives, not only opens them" @@ -16786,7 +16800,7 @@ "value": "Engine" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Motor de compresión" @@ -16834,7 +16848,7 @@ "value": "Jądro" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Motor" @@ -16894,7 +16908,7 @@ "value": "MacPacker includes several archive engines. The default is recommended; alternative engines can help with format-specific problems. Keep in mind that engine support varies by format." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "MacPacker hace uso de varios motores de compresión. El motor predeterminado es el recomendado; motores adicionales pueden ayudar con problemas específicos de un formato. Ten en cuenta que el soporte para cada motor varía dependiendo del formato." @@ -16942,7 +16956,7 @@ "value": "MacPacker zawiera w sobie kilka różnych silników archiwizujących. Rekomendowany jest domyślny; inne silniki mogą pomóc z problemami dla konkretnych formatów. Miej na uwadze, że wsparcie silnika różni się w zależności od formatu." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "MacPacker includes several archive engines. The default is recommended; alternative engines can help with format-specific problems. Keep in mind that engine support varies by format." @@ -17002,7 +17016,7 @@ "value": "Export Logs" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Exportar registros" @@ -17050,7 +17064,7 @@ "value": "Eksportuj dzienniki" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Exportar Registros" @@ -17110,7 +17124,7 @@ "value": "Extensions" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Extensiones" @@ -17158,7 +17172,7 @@ "value": "Rozszerzenia" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Extensões" @@ -17218,7 +17232,7 @@ "value": "External volumes" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Volúmenes externos" @@ -17266,7 +17280,7 @@ "value": "Dyski zewnętrzne" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Volumes externos" @@ -17326,7 +17340,7 @@ "value": "Covers USB drives, memory cards and network shares, which appear in /Volumes." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Incluye unidades USB, tarjetas de memoria y recursos compartidos de red que aparecen en /Volumes." @@ -17374,7 +17388,7 @@ "value": "Zawiera dyski USB, karty pamięci i udziały, ktore pojawiają się w /Volumes." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Covers USB drives, memory cards and network shares, which appear in /Volumes." @@ -17434,7 +17448,7 @@ "value": "Extract To…" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Extraer en…" @@ -17482,7 +17496,7 @@ "value": "Rozpakuj do…" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Extrair para…" @@ -17542,7 +17556,7 @@ "value": "Extract to folder" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Extraer en carpeta" @@ -17590,7 +17604,7 @@ "value": "Rozpakuj do folderu" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Extract to folder" @@ -17650,7 +17664,7 @@ "value": "File Format" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Formato de archivo" @@ -17698,7 +17712,7 @@ "value": "Format pliku" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Formato do arquivo" @@ -17758,7 +17772,7 @@ "value": "File provider extension" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Extensión de proveedor de archivos" @@ -17806,7 +17820,7 @@ "value": "Rozszerzenie typu pliku" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Extensão do File Provider" @@ -17866,7 +17880,7 @@ "value": "macOS lets %@ open and write files only where you allow it, which is why a folder-access panel shows up now and then. A grant covers everything inside the folder you pick, so allowing these two puts an end to the asking." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "macOS permite que %@ acceda a archivos solo en las ubicaciones que autorices. Por eso, ocasionalmente te pedirá acceso a una carpeta. El permiso se aplica a todo su contenido, así que autorizar estas dos ubicaciones evitará futuras solicitudes." @@ -17914,7 +17928,7 @@ "value": "macOS pozwala %@ otwierać i zapisywać pliki tylko tam, gdzie pozwolisz, dlatego czasami pojawia sie okno dostępu do folderów. Nadanie dostępu uwzględni wszystko w wybranym folderze i kolejne prośby nie będą sie pojawiać." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "macOS lets %@ open and write files only where you allow it, which is why a folder-access panel shows up now and then. A grant covers everything inside the folder you pick, so allowing these two puts an end to the asking." @@ -17974,7 +17988,7 @@ "value": "General" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "General" @@ -18022,7 +18036,7 @@ "value": "Ogólne" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Geral" @@ -18082,7 +18096,7 @@ "value": "Home folder" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Carpeta de inicio" @@ -18130,7 +18144,7 @@ "value": "Folder domowy" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Pasta pessoal" @@ -18190,7 +18204,7 @@ "value": "Covers the Desktop, Documents, Downloads and everything else in your home folder." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Incluye el Escritorio, Documentos, Descargas y todo lo demás en tu carpeta de inicio." @@ -18238,7 +18252,7 @@ "value": "Zawiera Pulpit, Dokumenty, Pobrane i wszystko inne w Twoim foldere domowym." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Covers the Desktop, Documents, Downloads and everything else in your home folder." @@ -18298,7 +18312,7 @@ "value": "How to set %@ as default?" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "¿Cómo establecer %@ como predeterminado?" @@ -18346,7 +18360,7 @@ "value": "Jak ustawić %@ jako domyślny?" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Como tornar %@ padrão" @@ -18406,7 +18420,7 @@ "value": "Include Beta updates" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Incluir actualizaciones beta" @@ -18454,7 +18468,7 @@ "value": "Dodaj wersje Beta" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Include Beta updates" @@ -18514,7 +18528,7 @@ "value": "Info on Engines" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Información de motores de compresión" @@ -18562,7 +18576,7 @@ "value": "Informacje o jądrach" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Informações" @@ -18622,7 +18636,7 @@ "value": "Logs" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Registros" @@ -18670,7 +18684,7 @@ "value": "Dzienniki" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Registros" @@ -18730,7 +18744,7 @@ "value": "Set MacPacker as the default app" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Establece MacPacker como la aplicación por defecto" @@ -18778,7 +18792,7 @@ "value": "Ustaw MacPacker jako domyślną aplikację" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Set MacPacker as the default app" @@ -18838,7 +18852,7 @@ "value": "Manage in System Settings" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Administra en la Configuración del Sistema" @@ -18886,7 +18900,7 @@ "value": "Zarządzaj w Ustawieniach Systemu" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Gerir em configurações do sistema" @@ -18946,7 +18960,7 @@ "value": "Nest actions in a submenu" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Agrupar acciones en un submenú" @@ -18994,7 +19008,7 @@ "value": "Zagnieżdżaj akcje w podmenu" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Nest actions in a submenu" @@ -19054,7 +19068,7 @@ "value": "Open cache directory" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Abrir directorio del caché" @@ -19102,7 +19116,7 @@ "value": "Otwórz katalog pamięci podręcznej" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Abrir diretório de cachê" @@ -19162,7 +19176,7 @@ "value": "Quit on last window closed" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Salir al cerrar la última ventana" @@ -19210,7 +19224,7 @@ "value": "Zakończ po zamknięciu ostatniego okna" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Quit on last window closed" @@ -19270,7 +19284,7 @@ "value": "Remember recent archives" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Recordar archivos comprimidos recientes" @@ -19318,7 +19332,7 @@ "value": "Zapamiętaj ostatnie archiwa" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Remember recent archives" @@ -19378,7 +19392,7 @@ "value": "Show application support folder" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Mostrar el folder the soporte de la aplicación" @@ -19426,7 +19440,7 @@ "value": "Pokaż folder pomocy technicznej aplikacji" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Mostrar pasta de suporte de aplicação" @@ -19486,7 +19500,7 @@ "value": "Show in Menu Bar" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Mostrar en la barra de menús" @@ -19534,7 +19548,7 @@ "value": "Pokazuj na pasku menu" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Mostrar na Barra de Menus" @@ -19594,7 +19608,7 @@ "value": "Show parent folder entry (“..”)" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Mostrar la carpeta superior (“..”)" @@ -19642,7 +19656,7 @@ "value": "Pokaż wpis folderu nadrzędnego (“..”)" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Show parent folder entry (“..”)" @@ -19702,7 +19716,7 @@ "value": "Smart extraction" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Extracción inteligente" @@ -19750,7 +19764,7 @@ "value": "Sprytne rozpakowywanie" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Smart extraction" @@ -19810,7 +19824,7 @@ "value": "Check for Updates…" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Buscar actualizaciones…" @@ -19858,7 +19872,7 @@ "value": "Sprawdź Aktualizacje…" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Verificar Atualizações" @@ -19918,7 +19932,7 @@ "value": "Changelog" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Registro de cambios" @@ -19966,7 +19980,7 @@ "value": "Dziennik zmian" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Changelog" @@ -20026,7 +20040,7 @@ "value": "Coming next" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Próximamente" @@ -20074,7 +20088,7 @@ "value": "Nadchodzące zmiany" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Coming next" @@ -20134,7 +20148,7 @@ "value": "Release highlights" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Novedades destacadas" @@ -20182,7 +20196,7 @@ "value": "Najważniejsze nowości" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Destaques da versão" @@ -20242,7 +20256,7 @@ "value": "Support %@" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Apoya a %@" @@ -20290,7 +20304,7 @@ "value": "Wesprzyj %@" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Apoie o %@" @@ -20350,7 +20364,7 @@ "value": "Your donation supports ongoing development." } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Tu donación apoya el desarrollo continuo." @@ -20398,7 +20412,7 @@ "value": "Twoja darowizna wspiera dalszy rozwój." } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Sua doação apoia o desenvolvimento contínuo." @@ -20458,7 +20472,7 @@ "value": "Help keep %@ growing" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Ayuda a que %@ siga creciendo" @@ -20506,7 +20520,7 @@ "value": "Pomóż rozwijać %@" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Ajude o %@ a crescer" @@ -20566,7 +20580,7 @@ "value": "Welcome to %@" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Bienvenido a %@" @@ -20614,7 +20628,7 @@ "value": "Witaj w aplikacji %@" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Bem-vindo ao %@" @@ -20674,7 +20688,7 @@ "value": "View all changes" } }, - "es-mx": { + "es-MX": { "stringUnit": { "state": "translated", "value": "Ver todos los cambios" @@ -20722,7 +20736,7 @@ "value": "Zobacz wszystkie zmiany" } }, - "pt-br": { + "pt-BR": { "stringUnit": { "state": "translated", "value": "Ver todas as alterações" diff --git a/Modules/Sources/Core/AppStorageKeys.swift b/Modules/Sources/Core/AppStorageKeys.swift index de564b5..3d4fe64 100644 --- a/Modules/Sources/Core/AppStorageKeys.swift +++ b/Modules/Sources/Core/AppStorageKeys.swift @@ -69,6 +69,9 @@ public enum Keys { public static func saveOptionsSettings(_ format: String) -> String { "saveOptionsSettings.\(format)" } /// Leave `.DS_Store` files out of archives. Off by default, as in Finder's /// own Compress. + /// Global compression choice, shared by saves, Quick Compress and Finder actions. + public static let excludeMacMetadata = "excludeMacMetadata" + public static let requireWindowsCompatibleNames = "requireWindowsCompatibleNames" public static let saveOptionsExcludeDSStore = "saveOptionsExcludeDSStore" // register defaults upon app start so that the archive table has a default it diff --git a/Modules/Sources/Core/ArchiveSaver.swift b/Modules/Sources/Core/ArchiveSaver.swift index 4e0d84f..7ffac6b 100644 --- a/Modules/Sources/Core/ArchiveSaver.swift +++ b/Modules/Sources/Core/ArchiveSaver.swift @@ -67,7 +67,7 @@ final actor ArchiveSaver { self.sourceAsOpened = sourceAsOpened self.target = target self.items = items - self.options = options + self.options = ArchiveSaveOptions.applyingFilePreferences(to: options) self.isSaveAs = isSaveAs self.sourcePassword = sourcePassword self.passwordResolver = passwordResolver diff --git a/Modules/Sources/Core/Settings/ArchiveSaveOptions.swift b/Modules/Sources/Core/Settings/ArchiveSaveOptions.swift index 05097d6..8f0e743 100644 --- a/Modules/Sources/Core/Settings/ArchiveSaveOptions.swift +++ b/Modules/Sources/Core/Settings/ArchiveSaveOptions.swift @@ -169,6 +169,18 @@ public final class ArchiveSaveOptions: ObservableObject { /// for Quick Compress may reach it. public var startPageOptions: CompressionOptions { .init(format: format) } + /// Apply file preferences at the write boundary, including Finder requests + /// that do not open a save panel. Never weaken an explicit caller's choice. + nonisolated public static func applyingFilePreferences( + to options: CompressionOptions, in defaults: UserDefaults = .standard + ) -> CompressionOptions { + var result = options + result.excludeMacMetadata = options.excludeMacMetadata || defaults.bool(forKey: Keys.excludeMacMetadata) + result.requireWindowsCompatibleNames = options.requireWindowsCompatibleNames + || (result.excludeMacMetadata && defaults.bool(forKey: Keys.requireWindowsCompatibleNames)) + return result + } + // MARK: Memory /// Stores the settings for the next save — every format's, the format diff --git a/Modules/Sources/Swift7zip/CompressionOptions.swift b/Modules/Sources/Swift7zip/CompressionOptions.swift index 896b08f..0592e28 100644 --- a/Modules/Sources/Swift7zip/CompressionOptions.swift +++ b/Modules/Sources/Swift7zip/CompressionOptions.swift @@ -154,6 +154,13 @@ public struct CompressionOptions: Sendable { /// anyone means to archive. Entries the archive holds already stay. public var excludeDSStore: Bool + /// Omit Mac metadata when writing, including existing sidecars on an update. + /// Resource forks, Finder tags, hidden flags and custom folder icons are lost. + public var excludeMacMetadata: Bool + + /// Refuse invalid or colliding Windows filenames before writing any output. + public var requireWindowsCompatibleNames: Bool + /// Whether this writes an encrypted archive. public var encrypts: Bool { !(password ?? "").isEmpty } @@ -170,7 +177,9 @@ public struct CompressionOptions: Sendable { wordSize: UInt32? = nil, solidBlockSize: UInt64? = nil, volumeSize: UInt64? = nil, - excludeDSStore: Bool = false + excludeDSStore: Bool = false, + excludeMacMetadata: Bool = false, + requireWindowsCompatibleNames: Bool = false ) { self.format = format self.level = level @@ -184,5 +193,7 @@ public struct CompressionOptions: Sendable { self.solidBlockSize = solidBlockSize self.volumeSize = volumeSize self.excludeDSStore = excludeDSStore + self.excludeMacMetadata = excludeMacMetadata + self.requireWindowsCompatibleNames = requireWindowsCompatibleNames } } diff --git a/Modules/Sources/Swift7zip/SevenZipWriter.swift b/Modules/Sources/Swift7zip/SevenZipWriter.swift index 91ed1fb..eedf9f2 100644 --- a/Modules/Sources/Swift7zip/SevenZipWriter.swift +++ b/Modules/Sources/Swift7zip/SevenZipWriter.swift @@ -52,7 +52,7 @@ extension SevenZipArchive { progress: WriteProgressHandler? = nil ) throws { var options = options - let items = options.excludeDSStore ? items.excludingDSStore() : items + let items = (options.excludeDSStore || options.excludeMacMetadata) ? items.excludingDSStore() : items let inPlace = source != nil && source!.standardizedFileURL == destination.standardizedFileURL if inPlace && (options.volumeSize ?? 0) > 0 { @@ -92,6 +92,7 @@ extension SevenZipArchive { // method, whatever `options` say. An encrypted source only when the copy // gets a password too — rebuilt without one, it would come out in plain. var resolved: [ResolvedItem] + var checkedNames: [(String, Bool)] = [] let rebuild: Bool do { // scoped, so the handle is closed again before anything is written @@ -150,7 +151,11 @@ extension SevenZipArchive { } // Resolve the diff into a full item list for the C bridge. - resolved = try resolveDiff(sourceArchive: sourceArchive, items: items) + resolved = try resolveDiff(sourceArchive: sourceArchive, items: items, excludeMacMetadata: options.excludeMacMetadata, scratch: scratch) + if options.requireWindowsCompatibleNames { + checkedNames = try windowsNames(resolved, source: sourceArchive) + try validateWindowsNames(checkedNames) + } if rebuild, let sourceArchive { // extracting is the first half of the work, writing the second let firstHalf = progress.map { report in @@ -160,7 +165,13 @@ extension SevenZipArchive { resolved, from: sourceArchive, into: scratch, progress: firstHalf) } } - resolved.append(contentsOf: try metadataSidecars(for: resolved, scratch: scratch)) + if !options.excludeMacMetadata { + let sidecars = try metadataSidecars(for: resolved, scratch: scratch) + if options.requireWindowsCompatibleNames { + try validateWindowsNames(checkedNames + windowsNames(sidecars, source: nil)) + } + resolved.append(contentsOf: sidecars) + } let writeProgress = rebuild ? progress.map { report in @@ -212,11 +223,43 @@ extension SevenZipArchive { modificationDate: Date?, posixPermissions: UInt16?) } + /// Capture names while the source is open, including kept entries and renames. + private static func windowsNames(_ items: [ResolvedItem], source: SevenZipArchive?) throws -> [(String, Bool)] { + let existing = Dictionary(uniqueKeysWithValues: try (source?.entries ?? []).map { ($0.index, $0) }) + return try items.map { item in + switch item { + case .keep(let index), .move(let index, _): + guard let source, let raw = sz_entry_path(source.handle.ref, index) else { + throw SevenZipError.writeFailed("Cannot verify an existing archive entry's name") + } + let path: String + if case .move(_, let newPath) = item { path = newPath } + else { path = String(cString: raw) } + return (path, existing[index]?.isDirectory ?? path.hasSuffix("/")) + case .addFile(let path, _, _, _), .addData(let path, _, _, _): + return (path, false) + case .addDirectory(let path, _, _, _): + return (path, true) + } + } + } + + /// Reject conflicts before opening output, including generated metadata paths. + private static func validateWindowsNames(_ names: [(String, Bool)]) throws { + let conflicts = WindowsArchiveNames.conflicts(in: names) + guard conflicts.isEmpty else { + throw SevenZipError.writeFailed("These archive names are not Windows-compatible. Rename them or turn off Windows-compatible filenames:\n" + + conflicts.map(\.debugDescription).joined(separator: "\n")) + } + } + /// Resolves a user-facing diff into the full list the C bridge expects. /// All source entries are kept unless explicitly removed or moved. private static func resolveDiff( sourceArchive: SevenZipArchive?, - items: [ArchiveUpdateItem] + items: [ArchiveUpdateItem], + excludeMacMetadata: Bool, + scratch: URL ) throws -> [ResolvedItem] { // Collect removals and moves from the diff. var removedIndices: Set = [] @@ -253,6 +296,11 @@ extension SevenZipArchive { throw SevenZipError.writeFailed( "Could not read entry count from source") } + // The existing reader identifies sidecars; a filename prefix alone + // must never turn an ordinary user's file into disposable metadata. + let finderFiles = excludeMacMetadata ? Set(try archive.entries.filter { + !$0.isDirectory && ((movedIndices[$0.index] ?? $0.path) as NSString).lastPathComponent == ".DS_Store" + }.map(\.index)) : Set() for i in 0..= 0, + try isStoredAppleDouble(i, in: archive, scratch: scratch) { continue } + } if sidecarTarget >= 0 && removedIndices.contains(UInt32(sidecarTarget)) { continue } if let newPath = movedIndices[i] { result.append(.move(sourceIndex: i, newPath: newPath)) @@ -274,6 +327,46 @@ extension SevenZipArchive { return result } + /// A sidecar-shaped name alone is not enough: users can have an ordinary + /// `._notes` beside `notes`. Read its header before removing stored data. + private static func isStoredAppleDouble(_ index: UInt32, in archive: SevenZipArchive, scratch: URL) throws -> Bool { + guard let rawPath = sz_entry_path(archive.handle.ref, index) else { return false } + let relative = String(cString: rawPath).split(separator: "/") + .filter { $0 != "." && $0 != ".." }.joined(separator: "/") + let folder = scratch.appendingPathComponent("metadata-check-" + String(index)) + try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) + var error: UnsafeMutablePointer? + let target = sz_sidecar_target(archive.handle.ref, index) + guard target >= 0, let targetPath = sz_entry_path(archive.handle.ref, UInt32(target)) else { return false } + if !archive.hasPassword, + try archive.entries.contains(where: { $0.index == UInt32(target) && $0.isEncrypted }) { + throw SevenZipError.passwordMissing + } + let targetRelative = String(cString: targetPath).split(separator: "/") + .filter { $0 != "." && $0 != ".." }.joined(separator: "/") + let indices = [UInt32(target), index].sorted() + let result = indices.withUnsafeBufferPointer { + sz_extract_entries(archive.handle.ref, $0.baseAddress, UInt32($0.count), folder.path, nil, nil, &error) + } + defer { if let error { free(error) } } + if result != SZ_EXTRACT_OK { + if result == SZ_EXTRACT_WRONG_PASSWORD { throw SevenZipError.passwordWrong } + throw SevenZipError.writeFailed(error.map { String(cString: $0) } ?? "Could not verify stored metadata") + } + let file = folder.appendingPathComponent(relative) + let attributes = try? FileManager.default.attributesOfItem(atPath: file.path) + if attributes == nil { + // A valid sidecar is consumed when the extractor folds it onto its + // target. A lookalike stays on disk and must be kept as user data. + return FileManager.default.fileExists(atPath: folder.appendingPathComponent(targetRelative).path) + } + guard attributes?[.type] as? FileAttributeType == .typeRegular else { return false } + let handle = try FileHandle(forReadingFrom: file) + defer { try? handle.close() } + let header = try handle.read(upToCount: 26) ?? Data() + return header.count == 26 && header.prefix(8) == Data([0, 5, 0x16, 7, 0, 2, 0, 0]) + } + // MARK: - Save As /// Turns every entry kept from the source into an addition read back from a diff --git a/Modules/Sources/Swift7zip/WindowsArchiveNames.swift b/Modules/Sources/Swift7zip/WindowsArchiveNames.swift new file mode 100644 index 0000000..101057d --- /dev/null +++ b/Modules/Sources/Swift7zip/WindowsArchiveNames.swift @@ -0,0 +1,49 @@ +import Foundation + +/// Conservative filename checks for ordinary Windows extraction tools. This is +/// not a promise about archive codecs, symbolic links or the chosen destination's +/// total path length. Nothing here renames an entry. +/// https://learn.microsoft.com/windows/win32/fileio/naming-a-file +enum WindowsArchiveNames { + /// All invalid or colliding entry paths, in a stable order for the error UI. + /// Implied directories participate too: `Docs/a` and `docs/b` must not merge. + static func conflicts(in entries: [(path: String, isDirectory: Bool)]) -> [String] { + struct Node { + let spelling: String + let entry: String + let isDirectory: Bool + } + var nodes: [String: Node] = [:] + var problems: Set = [] + let reserved = Set(["CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$"] + + ["COM", "LPT"].flatMap { prefix in + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "¹", "²", "³"].map { prefix + $0 } + }) + for entry in entries { + var path = entry.path + if entry.isDirectory && path.hasSuffix("/") { path.removeLast() } + let parts = path.split(separator: "/", omittingEmptySubsequences: false).map(String.init) + var prefixes: [String] = [] + for (index, part) in parts.enumerated() { + let base = part.split(separator: ".", omittingEmptySubsequences: false).first.map(String.init) ?? "" + if part.isEmpty || part == "." || part == ".." || part.hasSuffix(".") || part.hasSuffix(" ") + || part.utf16.count > 255 || reserved.contains(base.uppercased()) + || part.unicodeScalars.contains(where: { $0.value < 32 || "<>:\"\\|?*".unicodeScalars.contains($0) }) { + problems.insert(entry.path) + } + prefixes.append(part) + let spelling = prefixes.joined(separator: "/") + let key = spelling.precomposedStringWithCanonicalMapping.uppercased() + let directory = index < parts.count - 1 || entry.isDirectory + if let old = nodes[key] { + if old.spelling != spelling || old.isDirectory != directory || !directory { + problems.formUnion([old.entry, entry.path]) + } + } else { + nodes[key] = Node(spelling: spelling, entry: entry.path, isDirectory: directory) + } + } + } + return problems.sorted() + } +} diff --git a/Modules/Tests/CoreTests/MacMetadataOptionsTests.swift b/Modules/Tests/CoreTests/MacMetadataOptionsTests.swift new file mode 100644 index 0000000..f6eecda --- /dev/null +++ b/Modules/Tests/CoreTests/MacMetadataOptionsTests.swift @@ -0,0 +1,100 @@ +import Foundation +import Darwin +import Testing +import Swift7zip +@testable import Core + +extension AllCoreTests { + struct MacMetadataOptionsTests { + @Test @MainActor func metadataPreferenceIsOptIn() throws { + let defaults = isolatedDefaults() + let input = CompressionOptions(format: .zip) + #expect(!ArchiveSaveOptions.applyingFilePreferences(to: input, in: defaults).excludeMacMetadata) + defaults.set(true, forKey: Keys.excludeMacMetadata) + #expect(ArchiveSaveOptions.applyingFilePreferences(to: input, in: defaults).excludeMacMetadata) + defaults.set(false, forKey: Keys.excludeMacMetadata) + #expect(!ArchiveSaveOptions.applyingFilePreferences(to: input, in: defaults).excludeMacMetadata) + } + + @Test(arguments: [false, true]) func compressionHonorsMetadataChoice(_ exclude: Bool) async throws { + let root = try makeTempDir() + defer { try? FileManager.default.removeItem(at: root) } + let input = root.appendingPathComponent("text.txt") + try Data("contents".utf8).write(to: input) + setExtendedAttribute("com.apple.metadata:_kMDItemUserTags", Data("tag".utf8), at: input) + setExtendedAttribute("com.apple.ResourceFork", Data("resource".utf8), at: input) + #expect(chflags(input.path, UInt32(UF_HIDDEN)) == 0) + let archive = root.appendingPathComponent("out.zip") + try SevenZipArchive.writeArchive(source: nil, destination: archive, items: [ + .addFile(archivePath: "text.txt", diskPath: input), + .addData(archivePath: ".DS_Store", data: Data("finder".utf8)), + .addData(archivePath: "._ordinary.txt", data: Data("user data".utf8)) + ], options: .init(format: .zip, excludeMacMetadata: exclude)) + let out = root.appendingPathComponent("out") + try await extractWithOurEngine(archive, to: out) + #expect(try Data(contentsOf: out.appendingPathComponent("text.txt")) == Data("contents".utf8)) + #expect(FileManager.default.fileExists(atPath: out.appendingPathComponent(".DS_Store").path) == !exclude) + #expect(extendedAttribute("com.apple.metadata:_kMDItemUserTags", at: out.appendingPathComponent("text.txt")) == (exclude ? nil : Data("tag".utf8))) + #expect(extendedAttribute("com.apple.ResourceFork", at: out.appendingPathComponent("text.txt")) == (exclude ? nil : Data("resource".utf8))) + #expect(try out.appendingPathComponent("text.txt").resourceValues(forKeys: [.isHiddenKey]).isHidden == !exclude) + #expect(try Data(contentsOf: out.appendingPathComponent("._ordinary.txt")) == Data("user data".utf8)) + } + + @Test(arguments: [CompressionOptions.Format.zip, .sevenZ]) func cleanupKeepsExistingArchivesEncrypted(_ format: CompressionOptions.Format) async throws { + let root = try makeTempDir() + defer { try? FileManager.default.removeItem(at: root) } + let file = root.appendingPathComponent("text.txt") + try Data("private".utf8).write(to: file) + setExtendedAttribute("com.apple.metadata:_kMDItemUserTags", Data("tag".utf8), at: file) + let archive = root.appendingPathComponent("locked." + format.rawValue) + try SevenZipArchive.writeArchive(source: nil, destination: archive, items: [.addFile(archivePath: "text.txt", diskPath: file)], options: .init(format: format, password: "secret", encryptFileNames: format == .sevenZ)) + try SevenZipArchive.writeArchive(source: archive, destination: archive, items: [], options: .init(format: format, excludeMacMetadata: true), sourcePassword: "secret") + let opened = try SevenZipArchive(url: archive, password: "secret") + #expect(try opened.entries.allSatisfy(\.isEncrypted)) + if format == .sevenZ { #expect(throws: (any Error).self) { try SevenZipArchive(url: archive) } } + let out = root.appendingPathComponent("out") + try FileManager.default.createDirectory(at: out, withIntermediateDirectories: true) + try opened.extractAll(to: out) + #expect(try Data(contentsOf: out.appendingPathComponent("text.txt")) == Data("private".utf8)) + #expect(extendedAttribute("com.apple.metadata:_kMDItemUserTags", at: out.appendingPathComponent("text.txt")) == nil) + } + + @Test(arguments: [false, true]) func customFolderIconsFollowTheOption(_ exclude: Bool) async throws { + let root = try makeTempDir() + defer { try? FileManager.default.removeItem(at: root) } + let folder = root.appendingPathComponent("CustomFolder") + try makeFolderWithCustomIcon(at: folder, fork: Data("icon".utf8)) + let archive = root.appendingPathComponent("folder.zip") + try SevenZipArchive.writeArchive(source: nil, destination: archive, items: [ + .addDirectory(archivePath: "CustomFolder", diskPath: folder), + .addFile(archivePath: "CustomFolder/" + customIconFileName, diskPath: folder.appendingPathComponent(customIconFileName)) + ], options: .init(format: .zip, excludeMacMetadata: exclude)) + let out = root.appendingPathComponent("out") + try await extractWithOurEngine(archive, to: out) + #expect(finderFlags(at: out.appendingPathComponent("CustomFolder")) == (exclude ? nil : FinderFlag.hasCustomIcon)) + #expect(extendedAttribute("com.apple.ResourceFork", at: out.appendingPathComponent("CustomFolder/" + customIconFileName)) == (exclude ? nil : Data("icon".utf8))) + } + + @Test(arguments: [false, true]) func existingMetadataIsRemovedWhenRequested(_ saveAs: Bool) async throws { + let root = try makeTempDir() + defer { try? FileManager.default.removeItem(at: root) } + let input = root.appendingPathComponent("text.txt") + try Data("contents".utf8).write(to: input) + setExtendedAttribute("com.apple.metadata:_kMDItemUserTags", Data("tag".utf8), at: input) + let source = root.appendingPathComponent("source.zip") + try SevenZipArchive.writeArchive(source: nil, destination: source, items: [ + .addFile(archivePath: "text.txt", diskPath: input), + .addData(archivePath: ".DS_Store", data: Data("finder".utf8)), + .addData(archivePath: "notes", data: Data("notes".utf8)), + .addData(archivePath: "._notes", data: Data("ordinary".utf8)) + ], options: .init(format: .zip)) + let target = saveAs ? root.appendingPathComponent("copy.zip") : source + try SevenZipArchive.writeArchive(source: source, destination: target, items: [], options: .init(format: .zip, excludeMacMetadata: true)) + let out = root.appendingPathComponent("out") + try await extractWithOurEngine(target, to: out) + #expect(extendedAttribute("com.apple.metadata:_kMDItemUserTags", at: out.appendingPathComponent("text.txt")) == nil) + #expect(!FileManager.default.fileExists(atPath: out.appendingPathComponent(".DS_Store").path)) + #expect(try Data(contentsOf: out.appendingPathComponent("._notes")) == Data("ordinary".utf8)) + } + } +} diff --git a/Modules/Tests/CoreTests/WindowsArchiveNamesTests.swift b/Modules/Tests/CoreTests/WindowsArchiveNamesTests.swift new file mode 100644 index 0000000..9cfaba4 --- /dev/null +++ b/Modules/Tests/CoreTests/WindowsArchiveNamesTests.swift @@ -0,0 +1,99 @@ +import Foundation +import Testing +@testable import Swift7zip +@testable import Core + +extension AllCoreTests { + struct WindowsArchiveNamesTests { + @Test func rejectsReservedAndInvalidNames() { + let names = ["CON", "nul.txt", "LPT¹.log", "COM9", "trailing.", "space ", "bad:name", "a\\b", "a?b", "control\n", "/absolute", "../escape", "a//b"] + for name in names { + #expect(WindowsArchiveNames.conflicts(in: [(name, false)]) == [name]) + } + #expect(WindowsArchiveNames.conflicts(in: [("café/report.txt", false), ("console.txt", false), ("COM10", false)]).isEmpty) + } + + @Test func catchesCaseAndFileFolderCollisions() { + for names in [[("Readme", false), ("README", false)], + [("Docs/a", false), ("docs/b", false)], + [("name", false), ("name/child", false)], + [("same", false), ("same", false)]] { + #expect(!WindowsArchiveNames.conflicts(in: names).isEmpty) + } + #expect(WindowsArchiveNames.conflicts(in: [("docs", true), ("docs/a", false), ("docs/b", false)]).isEmpty) + } + + @Test func refusesBeforeChangingDestinationAndReportsAllNames() throws { + let root = try makeTempDir() + defer { try? FileManager.default.removeItem(at: root) } + let target = root.appendingPathComponent("existing.zip") + let sentinel = Data("existing destination".utf8) + try sentinel.write(to: target) + do { + try SevenZipArchive.writeArchive(destination: target, items: [ + .addData(archivePath: "CON.txt", data: Data()), + .addData(archivePath: "bad:name", data: Data()) + ], options: .init(format: .zip, requireWindowsCompatibleNames: true)) + Issue.record("Expected incompatible names to stop the write") + } catch { + #expect(error.localizedDescription.contains("CON.txt")) + #expect(error.localizedDescription.contains("bad:name")) + } + #expect(try Data(contentsOf: target) == sentinel) + } + + @Test func checksKeptEntriesAndRenamesBeforeSaving() throws { + let root = try makeTempDir() + defer { try? FileManager.default.removeItem(at: root) } + let source = root.appendingPathComponent("source.zip") + let target = root.appendingPathComponent("copy.zip") + // The opt-out must still allow names that are valid on macOS. + try SevenZipArchive.writeArchive(destination: source, items: [ + .addData(archivePath: "bad:name", data: Data("contents".utf8)) + ], options: .init(format: .zip)) + let original = try Data(contentsOf: source) + #expect(throws: (any Error).self) { + try SevenZipArchive.writeArchive(source: source, destination: target, items: [], + options: .init(format: .zip, requireWindowsCompatibleNames: true)) + } + #expect(!FileManager.default.fileExists(atPath: target.path)) + #expect(try Data(contentsOf: source) == original) + let entry = try #require(SevenZipArchive(url: source).entries.first) + try SevenZipArchive.writeArchive(source: source, destination: target, + items: [.move(sourceIndex: entry.index, newPath: "safe.txt")], + options: .init(format: .zip, requireWindowsCompatibleNames: true)) + #expect(try SevenZipArchive(url: target).entries.map(\.path) == ["safe.txt"]) + } + + /// Generated metadata participates in the final namespace before output is opened. + @Test(arguments: ["__MACOSX/._file", "__macosx/._FILE"]) + func generatedSidecarCollisionPreservesDestination(_ path: String) throws { + let root = try makeTempDir() + defer { try? FileManager.default.removeItem(at: root) } + let input = root.appendingPathComponent("file") + try Data("contents".utf8).write(to: input) + setExtendedAttribute("com.apple.ResourceFork", Data("resource".utf8), at: input) + let target = root.appendingPathComponent("out.zip") + let original = Data("existing destination".utf8) + try original.write(to: target) + #expect(throws: (any Error).self) { + try SevenZipArchive.writeArchive(destination: target, items: [ + .addFile(archivePath: "file", diskPath: input), + .addData(archivePath: path, data: Data("ordinary user data".utf8)) + ], options: .init(format: .zip, requireWindowsCompatibleNames: true)) + } + #expect(try Data(contentsOf: target) == original) + } + + @Test func optionIsOffByDefaultAndFollowsItsParent() { + let defaults = isolatedDefaults() + let options = CompressionOptions(format: .zip) + defaults.set(true, forKey: Keys.requireWindowsCompatibleNames) + #expect(!ArchiveSaveOptions.applyingFilePreferences(to: options, in: defaults).requireWindowsCompatibleNames) + defaults.set(true, forKey: Keys.excludeMacMetadata) + #expect(ArchiveSaveOptions.applyingFilePreferences(to: options, in: defaults).requireWindowsCompatibleNames) + defaults.set(false, forKey: Keys.excludeMacMetadata) + #expect(!ArchiveSaveOptions.applyingFilePreferences(to: options, in: defaults).requireWindowsCompatibleNames) + } + } +}