【2022年】PowerShellでExcelからAndroidのstrings.xmlを自動生成する

# 前置き

https://qiita.com/princess114514/items/a25920aeff1d86c04694

この最後で紹介した、「翻訳用の文字列を列挙したExcel」から
自動でstrings.xmlを作成するというので
実際どう作ればいいのかというのをPowershellを使って実装してみる

# なぜPowerShellなのか

まぁExcel読み込んでテキストファイル吐き出せれば
C#でもPythonでもBrainFuckでもぶっちゃけなんでもいいんだけど
PowerShellはデフォルトでWindowsに搭載されてて
余計な物やエディタをインストールする必要ないので、これがお手軽かなと
macとかは知らん。Python使えばいいんじゃない？（適当）

エディタはサクラエディタ使ってる
（batファイルがctrl+Bで実行できるので）

# ファイル構成

LangFileConverterというフォルダを作り、この階層にbatやps1や翻訳用excelをぶち込む

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2740385/bfb0014a-f97c-b8c6-ab78-9430efcd319a.png)

※）hello.ps1はテストで使ったゴミです

# 想定する翻訳Excel

・app sheet

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2740385/674e3ee3-a73b-7dd9-a1f7-b3bc299c2c66.png)

・system sheet
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2740385/9b3cc1a1-9841-ef0a-15c3-a3222cad6cf1.png)

・npc sheet
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2740385/82ab6f58-b093-6a51-485b-b240cf6b4597.png)

・item sheet
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2740385/6f59894f-c5f1-ad4e-d4d4-65638ead8f0e.png)



# execute.bat

make_strings_xml.ps1 を実行するための叩き台

~~~ execute.bat
PowerShell -ExecutionPolicy RemoteSigned .\make_strings_xml.ps1
pause
~~~

# make_strings_xml.ps1
まずは全体の処理

~~~ make_strings_xml.ps1
# 1つのIDに対する言語データ
class Data
{
	[string] $name
	[string] $jp
	[string] $en
}

# データのリスト
$data_list = New-Object 'System.Collections.Generic.List[Data]'

# 1枚分のシートを読み込む
function ReadSheet( $sheet )
{
	# 最終行を取得
	$lastRowIndex = $sheet.UsedRange.Rows.Count + 1
	
	# 1行目は無視する
	for ($i=2; $i -lt $lastRowIndex; $i++)
	{
		$id_string = $sheet.name + "_" + $sheet.Cells.Item($i, 1).Text
		
		$data = New-Object Data
		
		$data.name = $id_string
		$data.jp = $sheet.Cells.Item($i, 2).Text
		$data.en = $sheet.Cells.Item($i, 3).Text
		
		$data_list.Add($data)
	}

	
}

# Excel 読み込み処理
function ReadExcel()
{
	$excel = New-Object -ComObject Excel.Application
	$book = $null

	# 対象となるエクセルの絶対パス
	$excel_path = "C:\Users\h\Desktop\LangFileConverter\strings.xlsx"

	try
	{
		$excel.Visible = $false
	    $excel.DisplayAlerts = $false

		# エクセルを開く
	    $book = $excel.Workbooks.Open($excel_path)
	    
	    # シートの数だけ処理する
	    foreach($sheet in $book.Worksheets)
		{
	    	ReadSheet $sheet
		} 
	    
	} finally {
	    if ($book -ne $null) {
	        [void]$book.Close($false)
	        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($book)
	    }

	    # Excelの終了
	    [void]$excel.Quit()

	    # オブジェクトの開放
	    # ApplicationとBookに対して行えばよい
	    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel)
	}

}

# xml掃き出し処理
function OutputXml()
{
	$text_path = "C:\Users\h\Desktop\LangFileConverter\strings.xml"
	
	$output = "<resources>`r`n"
	
	# xml生成
	
	foreach( $d in $data_list )
	{
		$output += "`t<string-array name=`"$($d.name)`">`r`n"
		
		
			$output += "`t`t<item>$($d.jp)</item>`r`n"
			$output += "`t`t<item>$($d.en)</item>`r`n"
		
		
		$output += "`t</string-array>`r`n"
	}
	
	$output += "</resources>"
	
	#書き出し
	Write-Output ( $output | Out-File $text_path -Encoding default )
	
}

# 実行処理
ReadExcel
OutputXml
~~~

一応やろうと思えばオブジェクト指向っぽくかけるとは思うけど
スクリプト言語なので、スクリプト言語っぽく上から下に書きました

~~~ make_strings_xml.ps1
# 1つのIDに対する言語データ
class Data
{
	[string] $name
	[string] $jp
	[string] $en
}

# データのリスト
$data_list = New-Object 'System.Collections.Generic.List[Data]'
~~~

エクセルそのままでは使いづらいので
使いやすいようにデータ加工するためのクラスとリスト

~~~ make_strings_xml.ps1
# Excel 読み込み処理
function ReadExcel()
{
	$excel = New-Object -ComObject Excel.Application
	$book = $null

	# 対象となるエクセルの絶対パス
	$excel_path = "C:\Users\h\Desktop\LangFileConverter\strings.xlsx"

	try
	{
		$excel.Visible = $false
	    $excel.DisplayAlerts = $false

		# エクセルを開く
	    $book = $excel.Workbooks.Open($excel_path)
	    
	    # シートの数だけ処理する
	    foreach($sheet in $book.Worksheets)
		{
	    	ReadSheet $sheet
		} 
	    
	} finally {
	    if ($book -ne $null) {
	        [void]$book.Close($false)
	        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($book)
	    }

	    # Excelの終了
	    [void]$excel.Quit()

	    # オブジェクトの開放
	    # ApplicationとBookに対して行えばよい
	    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel)
	}

}
~~~

エクセルを読み込む処理
シートが複数あるので、シート単体への処理を別関数に分ける


~~~ make_strings_xml.ps1
# 1枚分のシートを読み込む
function ReadSheet( $sheet )
{
	# 最終行を取得
	$lastRowIndex = $sheet.UsedRange.Rows.Count + 1
	
	# 1行目は無視する
	for ($i=2; $i -lt $lastRowIndex; $i++)
	{
		$id_string = $sheet.name + "_" + $sheet.Cells.Item($i, 1).Text
		
		$data = New-Object Data
		
		$data.name = $id_string
		$data.jp = $sheet.Cells.Item($i, 2).Text
		$data.en = $sheet.Cells.Item($i, 3).Text
		
		$data_list.Add($data)
	}
}
~~~

１枚分のシートを読み込み、DataのListに変換する処理


~~~ make_strings_xml.ps1
# xml掃き出し処理
function OutputXml()
{
	$text_path = "C:\Users\h\Desktop\LangFileConverter\strings.xml"
	
	$output = "<resources>`r`n"
	
	# xml生成
	
	foreach( $d in $data_list )
	{
		$output += "`t<string-array name=`"$($d.name)`">`r`n"
		
		
			$output += "`t`t<item>$($d.jp)</item>`r`n"
			$output += "`t`t<item>$($d.en)</item>`r`n"
		
		
		$output += "`t</string-array>`r`n"
	}
	
	$output += "</resources>"
	
	#書き出し
	Write-Output ( $output | Out-File $text_path -Encoding default )
	
}
~~~

xmlを吐き出す処理
アプリによっては、xmlではなく、別の形式で文字列を管理する場合もあるので
どういう形式を求めるかによって、ここの処理は別物になると思う

xml形式以外で考えられるのは
C#やJavaとかで「nameをキーにしたDictionary」形式や
json形式での保存とか（webとかで使うのか？そっち方面の知識はない）
toml形式とかもアリかもしれない

# 実際に吐き出されたxml

~~~ strings.xml
<resources>
	<string-array name="app_name">
		<item>テストアプリケーション</item>
		<item>Test Application</item>
	</string-array>
	<string-array name="system_use_item">
		<item>{user}は{itemName}を使った</item>
		<item>The {user} used {itemName}.</item>
	</string-array>
	<string-array name="system_use_item_on_target">
		<item>{user}は{target}に{itemName}を使った</item>
		<item>The {user} used {itemName} on {target}.</item>
	</string-array>
	<string-array name="system_player_die">
		<item>{player}は力尽きた</item>
		<item>The {player} ran out of steam.</item>
	</string-array>
	<string-array name="npc_tadokoro_koji">
		<item>田所浩二</item>
		<item>Koji Tadokoro</item>
	</string-array>
	<string-array name="npc_hasegawa_ryota">
		<item>長谷川亮太</item>
		<item>Ryota Hasegawa</item>
	</string-array>
	<string-array name="npc_karasawa_takahiro">
		<item>唐澤貴洋</item>
		<item>Takahiro Karasawa</item>
	</string-array>
	<string-array name="npc_iwama_koiti">
		<item>岩間好一</item>
		<item>Yosikazu Iwama</item>
	</string-array>
	<string-array name="item_harb">
		<item>おハーブ</item>
		<item>herb</item>
	</string-array>
	<string-array name="item_portion">
		<item>ポーション</item>
		<item>portion</item>
	</string-array>
</resources>

~~~

# あえて実装しなかったもの

新たに言語を追加しても、自動で認識するような設計にはしませんでした
ソースとしてのわかりやすさを優先するため
やろうと思えばできると思うけど、数行処理を追加するだけの手間と天秤にかけて
自動にするための実装コストのほうが高そうなので、個人的にはおすすめできないと思います

# 参考

https://qiita.com/vicugna-pacos/items/54c5e20d3c3ffae94518

https://tex2e.github.io/blog/powershell/read-excel

https://stackoverflow.com/questions/57188491/loop-through-each-worksheet-in-excel-to-perform-tasks-in-powershell

https://bayashita.com/p/entry/show/91

http://www.vwnet.jp/windows/PowerShell/Format.htm