Updating Kebab Casing with PowerShell
Posted on October 2, 2024 by Michael Keane GallowayAt some point within the last year, one of the application maintainers at work reached out about updating all of our message keys to use camel casing instead of kebab casing. The translation vendors prefer to have camel casing to have C style identifiers in generated code. The maintainer ended up reminding us about it later as we patched the application for a hot patch. So I put in a user story in our backlog, and tried to find an opportunity to pay down this technical debt.
I thought I had found a good time to pull that story in when we were gearing up to add new languages. That way I could make the switch before QA had to retest new languages so we wouldn’t have to have multiple QA efforts for validating the translations. We could have the keys and new translations at the same time so QA is just validating that our pages are translated appropriately. Unfortunately, the timing end up with the most recent set up keys going to the vendor with kebab casing. That meant that the re-imported translations from the vendor also had kebab casing and a large portion of my effort to switch from kebab casing to camel casing.
Thankfully, I had already decided to automate a large portion of this problem. In total, there were approximately 16,000 tokens that I had to update to use the appropriate casing spread across 4 code repositories. Not wanting to do all of that by hand, I wrote a PowerShell script to convert all of our kebab cased message keys to camel case across both the resource files and our TypeScript code. That script is as follows:
param($messageKeys)
if ($messageKeys -eq $null) {
Write-Output "Message Keys invalid. Please use that required parameter."
exit;
}
if ((Test-Path $messageKeys) -ne $true) {
Write-Output "Please select a file that exists."
exit;
}
$keys = Get-Content $messageKeys | ForEach-Object { $_.Split(' ') } | Where-Object { $_.Contains('-') -and $_ -match '^".+":$' -and $_ -notmatch '^"\w\w-\w\w":$' } | ForEach-Object { $_.Replace(':', '')} | Sort-Object | Get-Unique;
$keyMapping = [System.Collections.Generic.Dictionary[string,string]]::new();
foreach($key in $keys){
$keyParts = $key.Split('-');
for($i = 1; $i -lt $keyParts.length; $i++){
$keyParts[$i] = $keyParts[$i].substring(0,1).toUpper() + $keyParts[$i].substring(1);
}
$newKey = [system.string]::Join('', $keyParts);
$keyMapping.Add($key, $newKey);
}
foreach ($file in (Get-ChildItem -Recurse ./src | Where-Object { $_.Name.Contains(".ts") })){
Write-Output "Scanning $($file.FullName)";
$content = [System.IO.File]::ReadAllText($file.FullName);
foreach($key in $keys){
$replacement = $keyMapping[$key];
if ($content.Contains($key)){
$content = $content.Replace($key, $replacement);
}
}
[System.IO.File]::WriteAllText($file.FullName, $content);
}
Due to some complications between the repositories, I had to handle multiple file formats for sourcing message keys which resulted in me choosing not to parse the files. I instead used some string manipulation in PowerShell to get all of the unique message keys in the following one liner:
$keys = Get-Content $messageKeys | ForEach-Object { $_.Split(' ') } | Where-Object { $_.Contains('-') -and $_ -match '^".+":$' -and $_ -notmatch '^"\w\w-\w\w":$' } | ForEach-Object { $_.Replace(':', '')} | Sort-Object | Get-Unique;
I then create a dictionary of kebab cased keys and the equivalent camel cased keys:
$keyMapping = [System.Collections.Generic.Dictionary[string,string]]::new();
foreach($key in $keys){
$keyParts = $key.Split('-');
for($i = 1; $i -lt $keyParts.length; $i++){
$keyParts[$i] = $keyParts[$i].substring(0,1).toUpper() + $keyParts[$i].substring(1);
}
$newKey = [system.string]::Join('', $keyParts);
$keyMapping.Add($key, $newKey);
}
Finally, I loaded all of the typescript files from src
directory, grabbed the content of the file as a string using the Dotnet IO library, and then just looped through all of the keys. If the key was present just do a string replace with the resulting camel cased key. After looping through all of the keys use the IO library to write out the file again. That’s the final loop here:
foreach ($file in (Get-ChildItem -Recurse ./src | Where-Object { $_.Name.Contains(".ts") })){
Write-Output "Scanning $($file.FullName)";
$content = [System.IO.File]::ReadAllText($file.FullName);
foreach($key in $keys){
$replacement = $keyMapping[$key];
if ($content.Contains($key)){
$content = $content.Replace($key, $replacement);
}
}
[System.IO.File]::WriteAllText($file.FullName, $content);
}
With how many files and tokens I had to update and the timing issue, I was very thankful to have this script. It would have taken days to make these changes, and after having the reversion another couple of days to handle that issue as well. This way spending a couple of hours to have a repeatable tool allowed me to reapply the conversion in a matter of minutes once we knew that our keys had reverted to kebab casing. I was also able to hand off the tool to two other teams that may need it as well.