Skip to content

Commit 30399d3

Browse files
committed
Add bilingual UI and GitHub release presentation assets
1 parent e97f376 commit 30399d3

18 files changed

Lines changed: 665 additions & 164 deletions

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [1.1.0] - 2026-03-11
6+
7+
### Added
8+
9+
- Multilingual UI support with in-app language selection:
10+
- French and English resources (`Localizable.strings`)
11+
- language preference persisted in settings
12+
- translated labels for main interface, statuses, collision policies, summaries and key dialogs
13+
- New app screenshots in `docs/images/` and README visual section.
14+
15+
### Changed
16+
17+
- Swift Package now declares localized resources (`defaultLocalization` + `Resources` processing).
18+
- README updated with screenshots and multilingual usage note.
19+
520
## [1.0.5] - 2026-03-11
621

722
### Added

Package.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import PackageDescription
33

44
let package = Package(
55
name: "MuniConvert",
6+
defaultLocalization: "fr",
67
platforms: [
78
.macOS(.v13)
89
],
@@ -18,7 +19,10 @@ let package = Package(
1819
targets: [
1920
.executableTarget(
2021
name: "MuniConvert",
21-
path: "Sources/MuniConvert"
22+
path: "Sources/MuniConvert",
23+
resources: [
24+
.process("Resources")
25+
]
2226
),
2327
.testTarget(
2428
name: "MuniConvertTests",

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ MuniConvert est une application macOS (Swift + SwiftUI) qui orchestre des conver
5454
- Détection et test de LibreOffice
5555
- Arrêt en cours de traitement
5656
- Mémorisation des derniers réglages
57+
- Interface multilingue (Français / English) avec sélection de langue dans l'app
58+
59+
## Captures d’écran
60+
61+
![Vue principale](docs/images/screenshot-main-view.png)
62+
![Conversion en cours](docs/images/screenshot-conversion-running.png)
63+
![Table des résultats](docs/images/screenshot-results-table.png)
64+
![Paramètres et langue](docs/images/screenshot-settings-locale.png)
5765

5866
## Le logiciel ne modifie jamais les originaux
5967

@@ -109,9 +117,10 @@ swift run MuniConvert
109117
1. Choisir un dossier source.
110118
2. Choisir le profil de conversion.
111119
3. Configurer les options (sous-dossiers, sortie, collisions).
112-
4. Optionnel : activer `Simulation seulement`.
113-
5. Cliquer sur `Analyser` puis `Lancer la conversion`.
114-
6. Contrôler le journal et exporter le log si nécessaire.
120+
4. Choisir la langue de l’interface dans `Paramètres` si besoin.
121+
5. Optionnel : activer `Simulation seulement`.
122+
6. Cliquer sur `Analyser` puis `Lancer la conversion`.
123+
7. Contrôler le journal et exporter le log si nécessaire.
115124

116125
## Structure du projet
117126

@@ -138,7 +147,7 @@ MuniConvert/
138147

139148
- Qualité de conversion dépendante de LibreOffice et des documents d’entrée.
140149
- Traitement séquentiel (pas de parallélisation dans ce MVP).
141-
- Pas encore de suite de tests unitaires automatisés.
150+
- Traduction actuellement fournie en 2 langues (FR/EN).
142151

143152
## Feuille de route courte
144153

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: GPL-3.0-only
2+
3+
import Foundation
4+
5+
enum AppLanguage: String, CaseIterable, Identifiable, Codable {
6+
case french = "fr"
7+
case english = "en"
8+
9+
var id: String { rawValue }
10+
11+
var localeIdentifier: String { rawValue }
12+
13+
func displayName(in language: AppLanguage) -> String {
14+
switch (self, language) {
15+
case (.french, .french):
16+
return "Français"
17+
case (.english, .french):
18+
return "Anglais"
19+
case (.french, .english):
20+
return "French"
21+
case (.english, .english):
22+
return "English"
23+
}
24+
}
25+
}

Sources/MuniConvert/Models/CollisionPolicy.swift

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,32 @@ enum CollisionPolicy: String, CaseIterable, Identifiable, Codable {
1010
var id: String { rawValue }
1111

1212
var displayName: String {
13+
displayName(language: .french)
14+
}
15+
16+
var compactDisplayName: String {
17+
compactDisplayName(language: .french)
18+
}
19+
20+
func displayName(language: AppLanguage) -> String {
1321
switch self {
1422
case .skipExisting:
15-
return "Ignorer si existe"
23+
return LocalizationService.tr("collision.skip", language: language)
1624
case .overwrite:
17-
return "Remplacer"
25+
return LocalizationService.tr("collision.overwrite", language: language)
1826
case .renameWithSuffix:
19-
return "Renommer automatiquement"
27+
return LocalizationService.tr("collision.rename", language: language)
2028
}
2129
}
2230

23-
var compactDisplayName: String {
31+
func compactDisplayName(language: AppLanguage) -> String {
2432
switch self {
2533
case .skipExisting:
26-
return "Ignorer"
34+
return LocalizationService.tr("collision.skip.compact", language: language)
2735
case .overwrite:
28-
return "Remplacer"
36+
return LocalizationService.tr("collision.overwrite.compact", language: language)
2937
case .renameWithSuffix:
30-
return "Renommer"
38+
return LocalizationService.tr("collision.rename.compact", language: language)
3139
}
3240
}
3341
}

Sources/MuniConvert/Models/ConversionStats.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@ struct ConversionStats {
1616
}
1717

1818
var summaryLine: String {
19-
"Scannés: \(totalScanned) | Correspondants: \(totalMatched) | Convertis: \(converted) | Ignorés: \(ignored) | Existant ignoré: \(skippedExisting) | Simulation: \(dryRun) | Erreurs: \(errors)"
19+
summaryLine(language: .french)
20+
}
21+
22+
func summaryLine(language: AppLanguage) -> String {
23+
LocalizationService.tr(
24+
"stats.summary",
25+
language: language,
26+
[
27+
totalScanned,
28+
totalMatched,
29+
converted,
30+
ignored,
31+
skippedExisting,
32+
dryRun,
33+
errors
34+
]
35+
)
2036
}
2137
}

Sources/MuniConvert/Models/LogEntry.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,23 @@ enum LogStatus: String, Codable, CaseIterable {
1111
case dryRun
1212

1313
var displayName: String {
14+
displayName(language: .french)
15+
}
16+
17+
func displayName(language: AppLanguage) -> String {
1418
switch self {
1519
case .matched:
16-
return "matched"
20+
return LocalizationService.tr("log_status.matched", language: language)
1721
case .ignored:
18-
return "ignored"
22+
return LocalizationService.tr("log_status.ignored", language: language)
1923
case .converted:
20-
return "converted"
24+
return LocalizationService.tr("log_status.converted", language: language)
2125
case .failed:
22-
return "failed"
26+
return LocalizationService.tr("log_status.failed", language: language)
2327
case .skippedExisting:
24-
return "skippedExisting"
28+
return LocalizationService.tr("log_status.skipped_existing", language: language)
2529
case .dryRun:
26-
return "dryRun"
30+
return LocalizationService.tr("log_status.dry_run", language: language)
2731
}
2832
}
2933
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"app.title" = "MuniConvert";
2+
"app.subtitle" = "Batch document conversion via LibreOffice";
3+
"status.libreoffice" = "LibreOffice";
4+
"status.found" = "found";
5+
"status.not_found" = "not found";
6+
"status.mode" = "Mode";
7+
"status.simulation" = "SIMULATION";
8+
"section.selection" = "Selection";
9+
"section.conversion" = "Conversion";
10+
"section.results" = "Results";
11+
"section.settings" = "Settings";
12+
"label.source_folder" = "Source folder";
13+
"label.output_folder" = "Output folder";
14+
"label.profile_search" = "Profile search";
15+
"label.conversion_type" = "Conversion type";
16+
"label.collision" = "Collision";
17+
"label.libreoffice" = "LibreOffice";
18+
"label.language" = "Language";
19+
"label.status" = "Status";
20+
"label.message" = "Message";
21+
"placeholder.profile_search" = "Example: doc, pdf, odt...";
22+
"placeholder.libreoffice_path" = "Path to soffice";
23+
"button.choose_source_folder" = "Choose folder";
24+
"button.choose_output_folder" = "Choose output folder";
25+
"button.analyze" = "Analyze";
26+
"button.start_conversion" = "Start conversion";
27+
"button.stop" = "Stop";
28+
"button.detect" = "Detect";
29+
"button.test_libreoffice" = "Test LibreOffice";
30+
"button.open_output_folder" = "Open output folder";
31+
"button.export_log" = "Export log (.txt)";
32+
"button.clear_log" = "Clear log";
33+
"toggle.include_subdirs" = "Include subfolders";
34+
"toggle.use_separate_output" = "Use a separate output folder";
35+
"toggle.preserve_tree" = "Preserve folder structure";
36+
"toggle.dry_run" = "Simulation only (no conversion)";
37+
"toggle.ignore_hidden" = "Ignore hidden files";
38+
"run.state" = "Batch state";
39+
"run.status.idle" = "Ready";
40+
"run.status.running" = "Running";
41+
"run.status.completed" = "Completed";
42+
"run.status.cancelled" = "Cancelled";
43+
"run.status.failed" = "Error";
44+
"picker.select" = "Select...";
45+
"picker.no_profile" = "No profile found";
46+
"box.simulation.title" = "SIMULATION MODE ACTIVE";
47+
"box.simulation.line" = "No real conversion command will be executed.";
48+
"box.profile_summary.title" = "Active profile summary";
49+
"box.sensitive.title" = "Sensitive settings";
50+
"box.blocked.title" = "Conversion unavailable: missing prerequisites";
51+
"box.available.title" = "Conversion available";
52+
"box.available.line" = "All prerequisites are met.";
53+
"status.state_found" = "Status: found";
54+
"status.state_not_found" = "Status: not found";
55+
"status.version_format" = "Version: %@";
56+
"language.french" = "French";
57+
"language.english" = "English";
58+
"collision.skip" = "Skip if exists";
59+
"collision.overwrite" = "Overwrite";
60+
"collision.rename" = "Auto rename";
61+
"collision.skip.compact" = "Skip";
62+
"collision.overwrite.compact" = "Overwrite";
63+
"collision.rename.compact" = "Rename";
64+
"log_status.matched" = "matched";
65+
"log_status.ignored" = "ignored";
66+
"log_status.converted" = "converted";
67+
"log_status.failed" = "failed";
68+
"log_status.skipped_existing" = "target exists";
69+
"log_status.dry_run" = "dry run";
70+
"dialog.confirm_conversion.title" = "Confirm conversion";
71+
"dialog.confirm.launch" = "Launch";
72+
"dialog.confirm.cancel" = "Cancel";
73+
"alert.ok" = "OK";
74+
"helper.collision_help" = "Policy when the target file already exists.";
75+
"summary.profile.none" = "No profile selected.";
76+
"summary.profile.source_filter" = "Active source filter: %@ (case-insensitive)";
77+
"summary.profile.target_extension" = "Target extension: .%@";
78+
"summary.profile.libreoffice_format" = "LibreOffice format: %@";
79+
"path.none.source" = "No source folder";
80+
"path.none.output" = "No output folder";
81+
"blocker.running" = "A process is already running.";
82+
"blocker.choose_source" = "Choose a source folder.";
83+
"blocker.choose_profile" = "Choose a conversion type.";
84+
"blocker.choose_output" = "Choose an output folder.";
85+
"blocker.libreoffice_required" = "LibreOffice is required for real conversion.";
86+
"sensitive.mode" = "Mode: %@";
87+
"sensitive.mode.dry" = "Simulation (no real conversion)";
88+
"sensitive.mode.real" = "Real conversion";
89+
"sensitive.subdirs" = "Subfolders: %@";
90+
"common.yes" = "Yes";
91+
"common.no" = "No";
92+
"sensitive.output" = "Output: %@";
93+
"sensitive.tree" = "Structure: %@";
94+
"sensitive.collision" = "Collision: %@";
95+
"output_mode.separate_undefined" = "Separate folder not defined";
96+
"output_mode.same_as_source" = "Same folder as source";
97+
"tree.preserved" = "Preserved";
98+
"tree.not_preserved" = "Not preserved";
99+
"tree.not_applicable" = "N/A (output in source folder)";
100+
"dialog.confirm.profile" = "Profile: %@";
101+
"dialog.confirm.mode.dry" = "Mode: Simulation (no file created).";
102+
"dialog.confirm.mode.real" = "Mode: Real conversion.";
103+
"dialog.confirm.source" = "Source: %@";
104+
"dialog.confirm.output" = "Output: %@";
105+
"dialog.confirm.tree" = "Structure: %@";
106+
"dialog.confirm.collision" = "Collision: %@";
107+
"dialog.confirm.no_modify" = "Original files are never modified or deleted.";
108+
"dialog.confirm.continue" = "Continue?";
109+
"panel.source.title" = "Choose a source folder";
110+
"panel.source.message" = "Select the folder containing files to convert.";
111+
"panel.output.title" = "Choose an output folder";
112+
"panel.output.message" = "Select the folder where converted files will be created.";
113+
"progress.analyzing" = "Analyzing";
114+
"progress.simulating" = "Simulation running";
115+
"progress.converting" = "Converting";
116+
"label.analysis" = "Analysis";
117+
"label.simulation" = "Simulation";
118+
"label.conversion" = "Conversion";
119+
"alert.analyze_impossible" = "Analysis unavailable";
120+
"alert.conversion_impossible" = "Conversion unavailable";
121+
"progress.cancelling" = "Cancelling...";
122+
"summary.cancelling" = "Cancellation requested...";
123+
"progress.ready" = "Ready";
124+
"summary.none" = "No process executed.";
125+
"summary.running" = "Processing...";
126+
"run.finished" = "%@ completed";
127+
"run.cancelled" = "%@ cancelled";
128+
"run.failed" = "%@ failed";
129+
"run.interrupted" = "%@ interrupted.";
130+
"alert.error" = "Error";
131+
"error.source_folder_none" = "No source folder";
132+
"error.output_folder_none" = "No output folder";
133+
"alert.path_missing" = "Missing path";
134+
"alert.path_missing_message" = "Enter a path to soffice before testing.";
135+
"alert.libreoffice_found" = "LibreOffice detected";
136+
"alert.libreoffice_found_message" = "Version: %@";
137+
"alert.libreoffice_not_found" = "LibreOffice not found";
138+
"alert.libreoffice_not_found_message" = "The provided path is not executable.";
139+
"alert.log_empty" = "Empty log";
140+
"alert.log_empty_message" = "No entries to export.";
141+
"panel.export_log.title" = "Export log";
142+
"alert.export_success" = "Export successful";
143+
"alert.export_success_message" = "Log exported to:\n%@";
144+
"alert.export_failed" = "Export failed";
145+
"alert.no_folder" = "No folder";
146+
"alert.no_folder_message" = "Select a source folder or an output folder.";
147+
"summary.line" = "%@ — Scanned: %d, Matched: %d, Converted: %d, Dry run: %d, Ignored: %d, Ignored (target exists): %d, Errors: %d.";
148+
"stats.summary" = "Scanned: %d | Matched: %d | Converted: %d | Ignored: %d | Existing skipped: %d | Dry run: %d | Errors: %d";
149+
"log.export.header" = "MuniConvert - Log";
150+
"log.export.date" = "Export date: %@";

0 commit comments

Comments
 (0)