Skip to content

Commit 2b86bea

Browse files
committed
Fix ResourceFileParser to preserve file order and structure
Problem: - Write() was re-ordering entries alphabetically - Removed all data elements, losing XSD schema - Caused unnecessary git diffs when editing files Solution: - Rewrote Write() to update entries in-place - Preserves original order and XML structure - Only modifies changed values - New entries added at end Tests: Added 5 comprehensive tests for file preservation Demo: Added demo.sh with backup/restore, recorded GIF Docs: Added demo GIF creation guide to CONTRIBUTING.md All 26 tests passing.
1 parent f252cba commit 2b86bea

6 files changed

Lines changed: 564 additions & 24 deletions

File tree

CONTRIBUTING.md

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -233,16 +233,24 @@ public void Validate_ShouldDetectMissingKeys_WhenTranslationIsMissing()
233233

234234
## Release Process
235235

236-
Releases are automated via GitHub Actions:
236+
Releases are fully automated via GitHub Actions:
237237

238-
1. Maintainer runs: `./bump-version.sh patch` (or `minor`/`major`)
239-
2. Commits version changes
240-
3. Pushes tag: `git tag release-patch && git push origin release-patch`
241-
4. GitHub Actions automatically:
242-
- Runs tests
243-
- Builds all platforms
244-
- Creates GitHub release
245-
- Uploads binaries
238+
1. Maintainer pushes a release tag: `git tag release-patch && git push origin release-patch`
239+
- Use `release-patch` for bug fixes (0.6.2 → 0.6.3)
240+
- Use `release-minor` for new features (0.6.2 → 0.7.0)
241+
- Use `release-major` for breaking changes (0.6.2 → 1.0.0)
242+
243+
2. GitHub Actions workflow automatically:
244+
- Bumps version in `.csproj` and `README.md`
245+
- Updates `CHANGELOG.md` with new version and date
246+
- Commits version changes back to main
247+
- Creates version tag (e.g., `v0.6.3`)
248+
- Runs all tests
249+
- Builds all 4 platforms (Linux/Windows x64/ARM64)
250+
- Creates GitHub release with binaries and changelog
251+
- Cleans up the trigger tag
252+
253+
**Note:** Contributors don't need to worry about version numbers or releases. Maintainers handle the release process.
246254

247255
See [BUILDING.md](BUILDING.md) for more details.
248256

@@ -286,6 +294,56 @@ git merge upstream/main
286294
git push origin main
287295
```
288296

297+
## Creating Demo GIF
298+
299+
If you need to update or recreate the demo GIF (e.g., after adding new features):
300+
301+
### Prerequisites
302+
303+
- `asciinema` for recording terminal sessions
304+
- `agg` for converting recordings to GIF
305+
306+
### Installation
307+
308+
```bash
309+
# Install asciinema (if not already installed)
310+
sudo apt-get install asciinema
311+
312+
# Download pre-built agg binary
313+
wget https://github.com/asciinema/agg/releases/download/v1.4.3/agg-x86_64-unknown-linux-gnu -O agg
314+
chmod +x agg
315+
sudo mv agg /usr/local/bin/
316+
```
317+
318+
### Recording Process
319+
320+
```bash
321+
# 1. Build the project first
322+
./build.sh
323+
324+
# 2. Set optimal terminal size (120x30)
325+
resize -s 30 120
326+
327+
# 3. Record the demo (runs demo.sh automatically)
328+
asciinema rec lrm-demo.cast -c ./demo.sh
329+
330+
# 4. Convert to GIF
331+
agg lrm-demo.cast lrm-demo.gif --speed 1.5 --font-size 14 --theme monokai
332+
333+
# 5. Move to assets folder
334+
mv lrm-demo.gif assets/
335+
336+
# 6. Commit the updated GIF
337+
git add assets/lrm-demo.gif
338+
git commit -m "Update demo GIF with latest features"
339+
```
340+
341+
**Notes:**
342+
- The `demo.sh` script automatically backs up and restores test data
343+
- Terminal size 120x30 is optimal for GitHub README display
344+
- Speed 1.5x provides good balance between watchability and brevity
345+
- Use `monokai` theme for consistency with existing demo
346+
289347
## Getting Help
290348

291349
- **Questions?** Open a [GitHub Discussion](https://github.com/nickprotop/LocalizationManager/discussions)

Core/ResourceFileParser.cs

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public ResourceFile Parse(LanguageInfo language)
8484
}
8585

8686
/// <summary>
87-
/// Writes a ResourceFile back to disk, preserving XML structure.
87+
/// Writes a ResourceFile back to disk, preserving XML structure and original entry order.
8888
/// </summary>
8989
/// <param name="resourceFile">The resource file to write.</param>
9090
/// <exception cref="InvalidOperationException">Thrown when writing fails.</exception>
@@ -97,26 +97,80 @@ public void Write(ResourceFile resourceFile)
9797
? XDocument.Load(resourceFile.Language.FilePath)
9898
: CreateNewResxDocument();
9999

100-
// Remove all existing data elements
101-
xdoc.Root?.Elements("data").Remove();
102-
103-
// Add updated data elements
104-
foreach (var entry in resourceFile.Entries.OrderBy(e => e.Key))
100+
var root = xdoc.Root;
101+
if (root == null)
105102
{
106-
var dataElement = new XElement("data",
107-
new XAttribute("name", entry.Key),
108-
new XAttribute(XNamespace.Xml + "space", "preserve"));
103+
throw new InvalidOperationException("Invalid XML structure: missing root element");
104+
}
109105

110-
// Add value element
111-
dataElement.Add(new XElement("value", entry.Value ?? string.Empty));
106+
// Build a dictionary of new entries for quick lookup
107+
var newEntries = resourceFile.Entries.ToDictionary(e => e.Key);
108+
var processedKeys = new HashSet<string>();
112109

113-
// Add comment if present
114-
if (!string.IsNullOrEmpty(entry.Comment))
110+
// Update existing data elements in place (preserves original order)
111+
foreach (var dataElement in root.Elements("data").ToList())
112+
{
113+
var key = dataElement.Attribute("name")?.Value;
114+
if (string.IsNullOrEmpty(key)) continue;
115+
116+
if (newEntries.TryGetValue(key, out var entry))
117+
{
118+
// Update existing entry in place
119+
var valueElement = dataElement.Element("value");
120+
if (valueElement != null)
121+
{
122+
valueElement.Value = entry.Value ?? string.Empty;
123+
}
124+
else
125+
{
126+
dataElement.Add(new XElement("value", entry.Value ?? string.Empty));
127+
}
128+
129+
// Update or add comment
130+
var commentElement = dataElement.Element("comment");
131+
if (!string.IsNullOrEmpty(entry.Comment))
132+
{
133+
if (commentElement != null)
134+
{
135+
commentElement.Value = entry.Comment;
136+
}
137+
else
138+
{
139+
dataElement.Add(new XElement("comment", entry.Comment));
140+
}
141+
}
142+
else if (commentElement != null)
143+
{
144+
// Remove comment if it's now empty
145+
commentElement.Remove();
146+
}
147+
148+
processedKeys.Add(key);
149+
}
150+
else
115151
{
116-
dataElement.Add(new XElement("comment", entry.Comment));
152+
// Remove entries that are no longer present
153+
dataElement.Remove();
117154
}
155+
}
156+
157+
// Add new entries that weren't in the original file (at the end)
158+
foreach (var entry in resourceFile.Entries)
159+
{
160+
if (!processedKeys.Contains(entry.Key))
161+
{
162+
var dataElement = new XElement("data",
163+
new XAttribute("name", entry.Key),
164+
new XAttribute(XNamespace.Xml + "space", "preserve"),
165+
new XElement("value", entry.Value ?? string.Empty));
166+
167+
if (!string.IsNullOrEmpty(entry.Comment))
168+
{
169+
dataElement.Add(new XElement("comment", entry.Comment));
170+
}
118171

119-
xdoc.Root?.Add(dataElement);
172+
root.Add(dataElement);
173+
}
120174
}
121175

122176
// Save with proper formatting

0 commit comments

Comments
 (0)