Upgrade to Pro — share decks privately, control downloads, hide ads and more …

「魔法少女まどか☆マギカ Magia Exedra」のグローバル展開を支える、開発チームと翻訳...

Avatar for gree_tech gree_tech PRO
September 01, 2025

「魔法少女まどか☆マギカ Magia Exedra」のグローバル展開を支える、開発チームと翻訳チームの「意識しない協創」を実現するローカライズシステム

CEDEC2025で発表された資料です。
https://cedec.cesa.or.jp/2025/timetable/detail/s67af0e71ed4e6/

Avatar for gree_tech

gree_tech PRO

September 01, 2025
Tweet

More Decks by gree_tech

Other Decks in Technology

Transcript

  1. |篠原 功 Isao SHINOHARA ◈ 2013年に株式会社ポケラボに入社、 現在は株式会社WFSに所属 ローカライズシステムのほか リソース管理システムなどを担当 ◈

    開発・運用: 「SINoALICE -シノアリス-」 「アサルトリリィ Last Bullet」 基盤: 「魔法少女まどか☆マギカ Magia Exedra」 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 自己紹介 - 5 -
  2. ◈ 課題と目標 ◈ 設計 ◈ 実装 ◈ 開発チームと翻訳チームの分業 ◈ ローカライズで困ったこと

    ◈ まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 目次 - 6 -
  3. ◈ 課題と目標 ◈ 設計 ◈ 実装 ◈ 開発チームと翻訳チームの分業 ◈ ローカライズで困ったこと

    ◈ まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 目次 - 7 -
  4. ◈ 課題と目標 ◈ 設計 ◈ 実装 ◈ 開発チームと翻訳チームの分業 ◈ ローカライズで困ったこと

    ◈ まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 目次 - 8 -
  5. ◈ 課題と目標 ◈ 設計 ◈ 実装 ◈ 開発チームと翻訳チームの分業 ◈ ローカライズで困ったこと

    ◈ まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 目次 - 9 -
  6. ◈ 課題と目標 ◈ 設計 ◈ 実装 ◈ 開発チームと翻訳チームの分業 ◈ ローカライズで困ったこと

    ◈ まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 目次 - 10 -
  7. ◈ 課題と目標 ◈ 設計 ◈ 実装 ◈ 開発チームと翻訳チームの分業 ◈ ローカライズで困ったこと

    ◈ まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 目次 - 11 -
  8. ◈ 課題と目標 ◈ 設計 ◈ 実装 ◈ 開発チームと翻訳チームの分業 ◈ ローカライズで困ったこと

    ◈ まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 目次 - 12 -
  9. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 問題点 ◈ 機能開発が忙しすぎる

    ◈ 散在する膨大な翻訳対象を管理しきれない ◈ スケジュールのコントロールが難しい ◈ 継続して追加、変更が発生する 運営型ゲームの多言語対応の難しさ - 17 -
  10. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 問題点 ◈ 機能開発が忙しすぎる

    ◈ 散在する膨大な翻訳対象を管理しきれない ◈ スケジュールのコントロールが難しい ◈ 継続して追加、変更が発生する 運営型ゲームの多言語対応の難しさ - 18 -
  11. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 機能開発が忙しすぎる ✓ 多言語対応がなくても余裕なんてない

    ✓ そこにタスクを上乗せできるわけない ✓ そもそも日本語版の開発がマスト ✓ 開発と並行で作業が発生し続ける 運営型ゲームの多言語対応の難しさ - 19 -
  12. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 問題点 ◈ 機能開発が忙しすぎる

    ◈ 散在する膨大な翻訳対象を管理しきれない ◈ スケジュールのコントロールが難しい ◈ 継続して追加、変更が発生する 運営型ゲームの多言語対応の難しさ - 20 -
  13. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 散在する膨大な翻訳対象を管理しきれない ✓ UIもデータもシナリオも対象は大量

    ✓ 1日分の変更量だって把握しきれない ✓ 変更する人も大勢いる ✓ 追加、変更を一元管理するのは難しい 運営型ゲームの多言語対応の難しさ - 21 -
  14. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 問題点 ◈ 機能開発が忙しすぎる

    ◈ 散在する膨大な翻訳対象を管理しきれない ◈ スケジュールのコントロールが難しい ◈ 継続して追加、変更が発生する 運営型ゲームの多言語対応の難しさ - 22 -
  15. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS スケジュールのコントロールが難しい ✓ いつ、どれくらいの量がくるのか予測できない

    ✓ リリース日程にも影響がでてしまう ✓ リリースを分けても問題がある 運営型ゲームの多言語対応の難しさ - 23 -
  16. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 問題点 ◈ 機能開発が忙しすぎる

    ◈ 散在する膨大な翻訳対象を管理しきれない ◈ スケジュールのコントロールが難しい ◈ 継続して追加、変更が発生する 運営型ゲームの多言語対応の難しさ - 24 -
  17. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 継続して追加、変更が発生する ✓ 終わったと思っても変更は発生する

    ✓ 変更が入れば翻訳作業も発生する ✓ チーム間で擦り合わせるのが難しい 運営型ゲームの多言語対応の難しさ - 25 -
  18. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 開発チームが開発に注力できること 開発チーム Developer

    Repository 開発チームの領域に関してはなるべく 変更はしない。 今まで通りの開発をしていればOK。 目指したローカライズシステムの姿 - 27 -
  19. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 翻訳チームのタイミングで翻訳対象を取得できること 開発チーム Developer

    Extractor Repository Text + Meta リポジトリからその時点での最新 のデータを全て抽出する。 開発チームを介さずに自由に実行 できる。 目指したローカライズシステムの姿 - 28 -
  20. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 追加、変更含めて全体像が把握できること 開発チーム 翻訳チーム

    Developer Translator Extractor Repository Management Tool Text + Meta 抽出データを翻訳チームの管理システ ムに取り込む。 日本語をキーにして管理することで重 複は省かれ、未登録の日本語だけが新 規として取り込まれる。 翻訳テキスト以外のメタ情報と合わせ て翻訳を行う。 目指したローカライズシステムの姿 - 29 -
  21. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 開発、翻訳チームの作業がお互いに影響を与えないこと 開発チーム 翻訳チーム

    Developer Translator Extractor Repository Management Tool Reflector Text + Meta 翻訳データは開発チームの作業とコンフリクト しないように、専用のファイルとしてコミット される。 プロダクトは翻訳データがあれば使うが、なけ ればそのまま動作するようにしておく。 目指したローカライズシステムの姿 - 30 -
  22. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 抽出機と反映機 開発チーム 翻訳チーム

    Developer Translator Repository Management Tool Text + Meta Extractor Reflector 目指したローカライズシステムの姿 - 31 -
  23. ◈ 課題と目標 ◈ 設計 ◈ 実装 ◈ 開発チームと翻訳チームの分業 ◈ ローカライズで困ったこと

    ◈ まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 目次 - 32 -
  24. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS どうやって元の位置に反映するか? 場所は違ってもテキストの変化は 一緒。

    「編成」をキーにして 「Assign」が取得できればいい。 翻訳テーブル Japanese English ユニオン Union 編成 Assign パーティ Party 設計 - 37 -
  25. | ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 動的反映と静的反映 Japanese English

    ユニオン Union パーティ Party クエスト Quests 翻訳テーブル 画面の初期化時に、各テキストコンポーネント自身が、 翻訳テーブルから取得する。 埋め込みなどの準備は必要ない。 設計 - 44 -
  26. ◈ 課題と目標 ◈ 設計 ◈ 実装 ◈ 開発チームと翻訳チームの分業 ◈ ローカライズで困ったこと

    ◈ まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 目次 - 47 -
  27. |使用技術 ◈ クライアント: Unity, C# ◈ サーバー: PHP, YAML ◈

    DB: Spanner ◈ ストリーミング動画: HLS 実装 - 50 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS
  28. |実装対象 ◈ Unity UIテキストコンポーネント ◈ C#ソースコード ◈ シナリオデータ(ScriptableObject) ◈ ストリーミング動画の字幕

    ◈ マスターデータ ◈ 設定ファイル(YAML) 実装ルール 抽出方法 表示・反映方 法 実装 - 51 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS
  29. |実装対象 ◈ Unity UIテキストコンポーネント ◈ C#ソースコード ◈ シナリオデータ(ScriptableObject) ◈ ストリーミング動画の字幕

    ◈ マスターデータ ◈ 設定ファイル(YAML) 実装ルール 抽出方法 表示・反映方 法 実装 - 52 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS
  30. ©WFS var guids = AssetDatabase.FindAssets("t:Scene t:Prefab"); foreach (var guid in

    guids) { var path = AssetDatabase.GUIDToAssetPath(guid); var asset = AssetDatabase.LoadMainAssetAtPath(path); if (asset is SceneAsset) { var scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Additive); foreach (var root in scene.GetRootGameObjects()) { foreach (var component in root.GetComponentsInChildren<TextMeshPro>(true)) { // Unityシーン: TextMeshProに入力された日本語テキストをリストに追加 texts.Add(component.text); } } EditorSceneManager.CloseScene(scene, true); } else if (asset is GameObject prefab) { foreach (var component in prefab.GetComponentsInChildren<TextMeshPro>(true)) { // Unityプレハブ: TextMeshProに入力された日本語テキストリストに追加 texts.Add(component.text); } } } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 56 -
  31. ©WFS var guids = AssetDatabase.FindAssets("t:Scene t:Prefab"); foreach (var guid in

    guids) { var path = AssetDatabase.GUIDToAssetPath(guid); var asset = AssetDatabase.LoadMainAssetAtPath(path); if (asset is SceneAsset) { var scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Additive); foreach (var root in scene.GetRootGameObjects()) { foreach (var component in root.GetComponentsInChildren<TextMeshPro>(true)) { // Unityシーン: TextMeshProに入力された日本語テキストをリストに追加 texts.Add(component.text); } } EditorSceneManager.CloseScene(scene, true); } else if (asset is GameObject prefab) { foreach (var component in prefab.GetComponentsInChildren<TextMeshPro>(true)) { // Unityプレハブ: TextMeshProに入力された日本語テキストリストに追加 texts.Add(component.text); } } } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 57 -
  32. ©WFS var scene = EditorSceneManager.OpenScene( path, OpenSceneMode.Additive); foreach (var root

    in scene.GetRootGameObjects()) { 〜〜〜〜〜〜〜〜〜〜〜 省略 〜〜〜〜〜〜〜〜〜〜〜〜〜〜 } EditorSceneManager.CloseScene(scene, true); ◈ 取得したいシーンをオープン ◈ ルートオブジェクトからGameObjectを取得 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 58 -
  33. ©WFS var scene = EditorSceneManager.OpenScene( path, OpenSceneMode.Additive); foreach (var root

    in scene.GetRootGameObjects()) { 〜〜〜〜〜〜〜〜〜〜〜 省略 〜〜〜〜〜〜〜〜〜〜〜〜〜〜 } EditorSceneManager.CloseScene(scene, true); ◈ 取得したいシーンをオープン ◈ ルートオブジェクトからGameObjectを取得 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 59 -
  34. ©WFS var scene = EditorSceneManager.OpenScene( path, OpenSceneMode.Additive); foreach (var root

    in scene.GetRootGameObjects()) { 〜〜〜〜〜〜〜〜〜〜〜 省略 〜〜〜〜〜〜〜〜〜〜〜〜〜〜 } EditorSceneManager.CloseScene(scene, true); ◈ 取得したいシーンをオープン ◈ ルートオブジェクトからGameObjectを取得 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 60 -
  35. ©WFS var components = root.GetComponentsInChildren<TextMeshPro>(true); foreach (var component in components)

    { // TextMeshProで入力された日本語テキストをリストに追加 texts.Add(component.text); } ◈ GameObjectからTextMeshProを取得 ◈ 入力された日本語テキストを抽出 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 61 -
  36. ©WFS var components = root.GetComponentsInChildren<TextMeshPro>(true); foreach (var component in components)

    { // TextMeshProで入力された日本語テキストをリストに追加 texts.Add(component.text); } ◈ GameObjectからTextMeshProを取得 ◈ 入力された日本語テキストを抽出 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 62 -
  37. ©WFS public class LocalizeTextComponent : MonoBehaviour { private void OnEnable()

    { var component = GetComponent<TextMeshPro>(); // 元の日本語テキストをハッシュ化して英語テキストを取得 var hash = Hash128.Compute(component.text).ToString(); var trans = LocalizeTextMasterData.GetText(hash); // 英語テキストに置き換え component.text = trans; } } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 64 -
  38. |実装対象 ◈ Unity UIテキストコンポーネント ◈ C#ソースコード ◈ シナリオデータ(ScriptableObject) ◈ ストリーミング動画の字幕

    ◈ マスターデータ ◈ 設定ファイル(YAML) 実装ルール 抽出方法 表示・反映方 法 実装 - 67 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS
  39. ©WFS // LocalizeDefineTextクラスのプロパティを取得 var flags = BindingFlags.Instance | BindingFlags.Static |

    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; var properties = typeof(LocalizeDefineText).GetProperties(flags); foreach (var property in properties) { // プロパティの値を取得してstringだった場合はリストに追加 switch (property.GetValue(null)) { case string str: texts.Add(str); break; case List<string> strList: texts.AddRange(strList); break; case IDictionary dict: var args = dict.GetType().IsGenericType ? dict.GetType().GetGenericArguments() : null; if (args != null && args.Length == 2 && args[1] == typeof(string)) { foreach (DictionaryEntry entry in dict) { texts.Add(entry.Key.ToString()); } } break; } } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 71 -
  40. ©WFS // LocalizeDefineTextクラスのプロパティを取得 var flags = BindingFlags.Instance | BindingFlags.Static |

    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; var properties = typeof(LocalizeDefineText).GetProperties(flags); foreach (var property in properties) { // プロパティの値を取得してstringだった場合はリストに追加 switch (property.GetValue(null)) { case string str: texts.Add(str); break; case List<string> strList: texts.AddRange(strList); break; case IDictionary dict: var args = dict.GetType().IsGenericType ? dict.GetType().GetGenericArguments() : null; if (args != null && args.Length == 2 && args[1] == typeof(string)) { foreach (DictionaryEntry entry in dict) { texts.Add(entry.Key.ToString()); } } break; } } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 72 -
  41. ©WFS // LocalizeDefineTextクラスのプロパティを取得 var flags = BindingFlags.Instance | BindingFlags.Static |

    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; var properties = typeof(LocalizeDefineText).GetProperties(flags); ◈ 取得したいプロパティの種類を指定 ◈ 条件に一致したクラスのプロパティ情報を取得 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 73 -
  42. ©WFS // LocalizeDefineTextクラスのプロパティを取得 var flags = BindingFlags.Instance | BindingFlags.Static |

    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; var properties = typeof(LocalizeDefineText).GetProperties(flags); ◈ 取得したいプロパティの種類を指定 ◈ 条件に一致したクラスのプロパティ情報を取得 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 74 -
  43. ©WFS // LocalizeDefineTextクラスのプロパティを取得 var flags = BindingFlags.Instance | BindingFlags.Static |

    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; var properties = typeof(LocalizeDefineText).GetProperties(flags); ◈ 取得したいプロパティの種類を指定 ◈ 条件に一致したクラスのプロパティ情報を取得 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 75 -
  44. ©WFS // プロパティの値を取得してstringだった場合はリストに追加 switch (property.GetValue(null)) { case string str: texts.Add(str);

    break; ◈ プロパティを実行して値を取得 ◈ stringだった場合はリストに追加 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 76 -
  45. ©WFS // プロパティの値を取得してstringだった場合はリストに追加 switch (property.GetValue(null)) { case string str: texts.Add(str);

    break; ◈ プロパティを実行して値を取得 ◈ stringだった場合はリストに追加 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 77 -
  46. ©WFS public static class LocalizeDefineText { public static string PresentInBox

    => Translate("報酬はプレゼントボックスへ送られます"); public static string PresentConfirmText => Translate("以下のプレゼントを受け取りました。"); public static string AddGoldMaxOver => Translate("獲得できるA-Qチップが最大を越えています"); } public static string Translate(string orig) { // 日本語テキストに紐づいた英語テキストを取得 var hash = Hash128.Compute(orig).ToString(); return LocalizeTextMasterData.GetText(hash); } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 79 -
  47. ©WFS public static class LocalizeDefineText { public static string PresentInBox

    => Translate("報酬はプレゼントボックスへ送られます"); public static string PresentConfirmText => Translate("以下のプレゼントを受け取りました。"); public static string AddGoldMaxOver => Translate("獲得できるA-Qチップが最大を越えています"); } public static string Translate(string orig) { // 日本語テキストに紐づいた英語テキストを取得 var hash = Hash128.Compute(orig).ToString(); return LocalizeTextMasterData.GetText(hash); } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 80 -
  48. ©WFS public static string PresentInBox => Translate("報酬はプレゼントボックスへ送られます"); —----------------------------------------------------- // プロパティの利用

    var data = new LoginBonusPopup.Data(text: PresentInBox); SystemUI.OpenPopup<LoginBonusPopup>(data); ◈ プロパティ形式にして翻訳メソッドを通す ◈ プロパティ利用時に翻訳を実行 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 81 -
  49. ©WFS public static string PresentInBox => Translate("報酬はプレゼントボックスへ送られます"); —----------------------------------------------------- // プロパティの利用

    var data = new LoginBonusPopup.Data(text: PresentInBox); SystemUI.OpenPopup<LoginBonusPopup>(data); ◈ プロパティ形式にして翻訳メソッドを通す ◈ プロパティ利用時に翻訳を実行 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 82 -
  50. ©WFS public static string PresentInBox => Translate("報酬はプレゼントボックスへ送られます"); —----------------------------------------------------- // プロパティの利用

    var data = new LoginBonusPopup.Data(text: PresentInBox); SystemUI.OpenPopup<LoginBonusPopup>(data); ◈ プロパティ形式にして翻訳メソッドを通す ◈ プロパティ利用時に翻訳を実行 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 83 -
  51. ©WFS public static string PresentInBox => Translate("報酬はプレゼントボックスへ送られます"); —----------------------------------------------------- // プロパティの利用

    var data = new LoginBonusPopup.Data(text: PresentInBox); SystemUI.OpenPopup<LoginBonusPopup>(data); ◈ プロパティ形式にして翻訳メソッドを通す ◈ プロパティ利用時に翻訳を実行 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 84 -
  52. ©WFS public static string PresentInBox => Translate("報酬はプレゼントボックスへ送られます"); —----------------------------------------------------- // プロパティの利用

    var data = new LoginBonusPopup.Data(text: PresentInBox); SystemUI.OpenPopup<LoginBonusPopup>(data); ◈ プロパティ形式にして翻訳メソッドを通す ◈ プロパティ利用時に翻訳を実行 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 85 -
  53. |実装対象 ◈ Unity UIテキストコンポーネント ◈ C#ソースコード ◈ シナリオデータ(ScriptableObject) ◈ ストリーミング動画の字幕

    ◈ マスターデータ ◈ 設定ファイル(YAML) 実装ルール 抽出方法 表示・反映方 法 実装 - 87 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS
  54. ©WFS var texts = new List<string>(); var guids = AssetDatabase.FindAssets

    ($"t:{typeof(ScenarioScriptableObject}"); foreach (var guid in guids) { // シナリオ(ScriptableObject)をロード var path = AssetDatabase.GUIDToAssetPath(guid); var scenario = AssetDatabase.LoadAssetAtPath( path, typeof(ScenarioScriptableObject)); // シナリオ内のセリフをリストに追加 var serifs = scenario.GetSerifs(); texts.AddRange(serifs); ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 92 -
  55. ©WFS var texts = new List<string>(); var guids = AssetDatabase.FindAssets

    ($"t:{typeof(ScenarioScriptableObject}"); foreach (var guid in guids) { // シナリオ(ScriptableObject)をロード var path = AssetDatabase.GUIDToAssetPath(guid); var scenario = AssetDatabase.LoadAssetAtPath( path, typeof(ScenarioScriptableObject)); // シナリオ内のセリフをリストに追加 var serifs = scenario.GetSerifs(); texts.AddRange(serifs); ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 93 -
  56. ©WFS var guids = AssetDatabase.FindAssets($"t:{typeof(ScenarioScriptableObject)}"); foreach (var guid in guids)

    { var path = AssetDatabase.GUIDToAssetPath(guid); var scenario = AssetDatabase.LoadAssetAtPath(path, typeof(ScenarioScriptableObject)) as ScenarioScriptableObject; var serifs = scenario.GetSerifs(); for (int lineNumber = 1; 1 <= serifs.Count; lineNumber++) { var serif = serifs[lineNumber - 1]; // シナリオ内のセリフを英語テキストに置き換え var translatedSerif = Translate(serif); scenario.Replace(lineNumber, translatedSerif); } // 英語用のシナリオをJSONファイルとして保存 var json = JsonUtility.ToJson(scenario, true); using StreamWriter writer = new StreamWriter(filePath); writer.Write(json); } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 95 -
  57. ©WFS var guids = AssetDatabase.FindAssets($"t:{typeof(ScenarioScriptableObject)}"); foreach (var guid in guids)

    { var path = AssetDatabase.GUIDToAssetPath(guid); var scenario = AssetDatabase.LoadAssetAtPath(path, typeof(ScenarioScriptableObject)) as ScenarioScriptableObject; var serifs = scenario.GetSerifs(); for (int lineNumber = 1; 1 <= serifs.Count; lineNumber++) { var serif = serifs[lineNumber - 1]; // シナリオ内のセリフを英語テキストに置き換え var translatedSerif = Translate(serif); scenario.Replace(lineNumber, translatedSerif); } // 英語用のシナリオをJSONファイルとして保存 var json = JsonUtility.ToJson(scenario, true); using StreamWriter writer = new StreamWriter(filePath); writer.Write(json); } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 96 -
  58. ©WFS var guids = AssetDatabase.FindAssets($"t:{typeof(ScenarioScriptableObject)}"); foreach (var guid in guids)

    { var path = AssetDatabase.GUIDToAssetPath(guid); var scenario = AssetDatabase.LoadAssetAtPath(path, typeof(ScenarioScriptableObject)) as ScenarioScriptableObject; var serifs = scenario.GetSerifs(); for (int lineNumber = 1; 1 <= serifs.Count; lineNumber++) { var serif = serifs[lineNumber - 1]; // シナリオ内のセリフを英語テキストに置き換え var translatedSerif = Translate(serif); scenario.Replace(lineNumber, translatedSerif); } // 英語用のシナリオをJSONファイルとして保存 var json = JsonUtility.ToJson(scenario, true); using StreamWriter writer = new StreamWriter(filePath); writer.Write(json); } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 97 -
  59. ©WFS // 英語用のシナリオをJSONファイルとして保存 var json = JsonUtility.ToJson(scenario, true); using StreamWriter

    writer = new StreamWriter(filePath); writer.Write(json); ◈ ScriptableObjectからJSONへの変換 ◈ JSONファイルとして保存 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 98 -
  60. ©WFS // 英語用のシナリオをJSONファイルとして保存 var json = JsonUtility.ToJson(scenario, true); using StreamWriter

    writer = new StreamWriter(filePath); writer.Write(json); ◈ ScriptableObjectからJSONへの変換 ◈ JSONファイルとして保存 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 99 -
  61. ©WFS // JSONファイルからScriptableObjectを復元 var ins = ScriptableObject .CreateInstance<ScenarioScriptableObject>(); JsonUtility.FromJsonOverwrite(json, ins);

    ◈ JSONからScriptableObjectへの変換 ◈ ScriptableObjectインスタンスにオーバーライト ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 100 -
  62. ©WFS // JSONファイルからScriptableObjectを復元 var ins = ScriptableObject .CreateInstance<ScenarioScriptableObject>(); JsonUtility.FromJsonOverwrite(json, ins);

    ◈ JSONからScriptableObjectへの変換 ◈ ScriptableObjectインスタンスにオーバーライト ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 101 -
  63. ©WFS // JSONファイルからScriptableObjectを復元 var ins = ScriptableObject .CreateInstance<ScenarioScriptableObject>(); JsonUtility.FromJsonOverwrite(json, ins);

    ◈ JSONからScriptableObjectへの変換 ◈ ScriptableObjectインスタンスにオーバーライト ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 102 -
  64. |実装対象 ◈ Unity UIテキストコンポーネント ◈ C#ソースコード ◈ シナリオデータ(ScriptableObject) ◈ ストリーミング動画の字幕

    ◈ マスターデータ ◈ 設定ファイル(YAML) 実装ルール 抽出方法 表示・反映方 法 実装 - 104 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS
  65. ◈ File Signature ◈ 空行 ◈ キュータイミング ◈ 字幕テキスト |HLS

    .vttファイル (WebVTTフォーマット) ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ストリーミング動画の字幕 - 106 -
  66. ©WFS var filePaths = Directory.GetFiles(directoryPath, "*.vtt", SearchOption.AllDirectories); foreach (var filePath

    in filePaths) { var content = File.ReadAllText(filePath); var lines = content.Split(new[] {"¥r¥n", "¥r", "¥n"}, StringSplitOptions.None).ToList(); foreach (var line in lines) { // File Signature or 空行 or キュータイミング はスキップ var pat = @"^¥s*(¥d{1,2}:)?¥d{2}:¥d{2}¥.¥d{3}¥s*-->¥s*(¥d{1,2}:)?¥d{2}:¥d{2}¥.¥d{3}¥s*$"; if (line == "WEBVTT" || string.IsNullOrEmpty(line) || Regex.IsMatch(line, pat)) { continue; } // 字幕テキストを取得 texts.Add(line); } } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ストリーミング動画の字幕 - 109 -
  67. ©WFS var filePaths = Directory.GetFiles(directoryPath, "*.vtt", SearchOption.AllDirectories); foreach (var filePath

    in filePaths) { var content = File.ReadAllText(filePath); var lines = content.Split(new[] {"¥r¥n", "¥r", "¥n"}, StringSplitOptions.None).ToList(); foreach (var line in lines) { // File Signature or 空行 or キュータイミング はスキップ var pat = @"^¥s*(¥d{1,2}:)?¥d{2}:¥d{2}¥.¥d{3}¥s*-->¥s*(¥d{1,2}:)?¥d{2}:¥d{2}¥.¥d{3}¥s*$"; if (line == "WEBVTT" || string.IsNullOrEmpty(line) || Regex.IsMatch(line, pat)) { continue; } // 字幕テキストを取得 texts.Add(line); } } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ストリーミング動画の字幕 - 110 -
  68. ©WFS var content = File.ReadAllText(filePath); var lines = content.Split(new[] {"¥r¥n",

    "¥r", "¥n"}, StringSplitOptions.None).ToList(); foreach (var line in lines) { // File Signature or 空行 or キュータイミング var pat = @"^¥s*(¥d{1,2}:)?¥d{2}:¥d{2}¥.¥d{3}¥s*-->¥s*(¥d{1,2}:)?¥d{2}:¥d{2}¥.¥d{3}¥s*$"; if (line == "WEBVTT" || string.IsNullOrEmpty(line) || Regex.IsMatch(line, pat)) { texts.Add(line); continue; } // 字幕テキストは英語テキストに置き換え var translatedLine = Translate(line); texts.Add(translatedLine); } // 英語テキストをファイルに保存 File.WriteAllText(filePath, string.Join("¥n", texts)); ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ストリーミング動画の字幕 - 112 -
  69. ©WFS var content = File.ReadAllText(filePath); var lines = content.Split(new[] {"¥r¥n",

    "¥r", "¥n"}, StringSplitOptions.None).ToList(); foreach (var line in lines) { // File Signature or 空行 or キュータイミング var pat = @"^¥s*(¥d{1,2}:)?¥d{2}:¥d{2}¥.¥d{3}¥s*-->¥s*(¥d{1,2}:)?¥d{2}:¥d{2}¥.¥d{3}¥s*$"; if (line == "WEBVTT" || string.IsNullOrEmpty(line) || Regex.IsMatch(line, pat)) { texts.Add(line); continue; } // 字幕テキストは英語テキストに置き換え var translatedLine = Translate(line); texts.Add(translatedLine); } // 英語テキストをファイルに保存 File.WriteAllText(filePath, string.Join("¥n", texts)); ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ストリーミング動画の字幕 - 113 -
  70. |実装対象 ◈ Unity UIテキストコンポーネント ◈ C#ソースコード ◈ シナリオデータ(ScriptableObject) ◈ ストリーミング動画の字幕

    ◈ マスターデータ ◈ 設定ファイル(YAML) 実装ルール 抽出方法 表示・反映方 法 実装 - 115 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS
  71. ©WFS $database = (new SpannerClient())->connect('instanceId', 'databaseId'); // STRING型カラムを取得 $indexColumns =

    $database->execute(" SELECT column_name FROM information_schema.columns WHERE table_name = 'SkillMst' AND spanner_type LIKE 'STRING%'"); $stringColumns = array_column($indexColumns, 'column_name'); // 日本語版テーブルからSTRING型カラムの値をリストに追加 $records = $database->execute("SELECT * FROM SkillMst"); foreach ($records as $record) { foreach ($stringColumns as $column) { $texts[] = $record[$column]; } } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS マスターデータ - 119 -
  72. ©WFS $database = (new SpannerClient())->connect('instanceId', 'databaseId'); // STRING型カラムを取得 $indexColumns =

    $database->execute(" SELECT column_name FROM information_schema.columns WHERE table_name = 'SkillMst' AND spanner_type LIKE 'STRING%'"); $stringColumns = array_column($indexColumns, 'column_name'); // 日本語版テーブルからSTRING型カラムの値をリストに追加 $records = $database->execute("SELECT * FROM SkillMst"); foreach ($records as $record) { foreach ($stringColumns as $column) { $texts[] = $record[$column]; } } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS マスターデータ - 120 -
  73. ©WFS // STRING型カラムを取得 $indexColumns = $database->execute(" SELECT column_name FROM information_schema.columns

    WHERE table_name = 'SkillMst' AND spanner_type LIKE 'STRING%'"); ◈ テーブルのスキーマ情報からSTRING型カラムを取得 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS マスターデータ - 121 -
  74. ©WFS // STRING型カラムを取得 $indexColumns = $database->execute(" SELECT column_name FROM information_schema.columns

    WHERE table_name = 'SkillMst' AND spanner_type LIKE 'STRING%'"); ◈ テーブルのスキーマ情報からSTRING型カラムを取得 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS マスターデータ - 122 -
  75. ©WFS // STRING型カラムを取得 $indexColumns = $database->execute(" SELECT column_name FROM information_schema.columns

    WHERE table_name = 'SkillMst' AND spanner_type LIKE 'STRING%'"); ◈ テーブルのスキーマ情報からSTRING型カラムを取得 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS マスターデータ - 123 -
  76. ©WFS $database = (new SpannerClient())->connect('instanceId', 'databaseId'); // STRING型カラムを取得 $indexColumns =

    $database->execute(" SELECT column_name FROM information_schema.columns WHERE table_name = 'SkillMst' AND spanner_type LIKE 'STRING%'"); $stringColumns = array_column($indexColumns, 'column_name'); $records = $database->execute("SELECT * FROM SkillMst"); foreach ($records as $rowIndex => $record) { foreach ($stringColumns as $column) { // 日本語テーブルのSTRING型カラムを英語テキストに置き換え $records[$rowIndex][$column] = translate($value); } } // 英語テーブルを更新 $database->insertOrUpdateBatch(SkillMst_en_Latn, [$records]); ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS マスターデータ - 125 -
  77. ©WFS $database = (new SpannerClient())->connect('instanceId', 'databaseId'); // STRING型カラムを取得 $indexColumns =

    $database->execute(" SELECT column_name FROM information_schema.columns WHERE table_name = 'SkillMst' AND spanner_type LIKE 'STRING%'"); $stringColumns = array_column($indexColumns, 'column_name'); $records = $database->execute("SELECT * FROM SkillMst"); foreach ($records as $rowIndex => $record) { foreach ($stringColumns as $column) { // 日本語テーブルのSTRING型カラムを英語テキストに置き換え $records[$rowIndex][$column] = translate($value); } } // 英語テーブルを更新 $database->insertOrUpdateBatch(SkillMst_en_Latn, [$records]); ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS マスターデータ - 126 -
  78. |実装対象 ◈ Unity UIテキストコンポーネント ◈ C#ソースコード ◈ シナリオデータ(ScriptableObject) ◈ ストリーミング動画の字幕

    ◈ マスターデータ ◈ 設定ファイル(YAML) 実装ルール 抽出方法 表示・反映方 法 実装 - 128 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS
  79. ©WFS $yamlData = yaml_parse_file($yamlPath); // 多次元配列を1次元配列にフラット化 $data = $this->flatten($yamlData); foreach

    ($data as $key => $value) { // yamlの値が翻訳対象の場合は日本語テキストを取得 $pattern = '/[ぁ-ん]+|[ァ-ヴー]+|[一-龠]/u'; if (preg_match($pattern, $value)) { $texts[] = $value; } } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 設定ファイル(YAML) - 132 -
  80. ©WFS $yamlData = yaml_parse_file($yamlPath); // 多次元配列を1次元配列にフラット化 $data = $this->flatten($yamlData); foreach

    ($data as $key => $value) { // yamlの値が翻訳対象の場合は日本語テキストを取得 $pattern = '/[ぁ-ん]+|[ァ-ヴー]+|[一-龠]/u'; if (preg_match($pattern, $value)) { $texts[] = $value; } } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 設定ファイル(YAML) - 133 -
  81. ©WFS private function translateRecursive($yamlData) { foreach ($yamlData as $index =>

    $data) { if (is_array($data)) { $yamlData[$index] = $this->translateRecursive($data); } else if (preg_match('/[ぁ-んァ-ヴー一-龠]/u', $data)) { // yamlの値が翻訳対象の場合は翻訳テキストに置き換え $hashKey = md5($data); $yamlData[$index] = $this->translate($hashKey); } } return $yamlData; } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 設定ファイル(YAML) - 135 -
  82. ©WFS private function translateRecursive($yamlData) { foreach ($yamlData as $index =>

    $data) { if (is_array($data)) { $yamlData[$index] = $this->translateRecursive($data); } else if (preg_match('/[ぁ-んァ-ヴー一-龠]/u', $data)) { // yamlの値が翻訳対象の場合は翻訳テキストに置き換え $hashKey = md5($data); $yamlData[$index] = $this->translate($hashKey); } } return $yamlData; } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 設定ファイル(YAML) - 136 -
  83. ◈ 課題と目標 ◈ 設計 ◈ 実装 ◈ 開発チームと翻訳チームの分業 ◈ ローカライズで困ったこと

    ◈ まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 目次 - 141 -
  84. |使用技術 ◈ Googleスプレッドシート ◈ Google Apps Script ◈ Jenkins ◈

    Slack 開発チームと翻訳チームの分業 - 143 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS
  85. 開発チーム 翻訳チーム Developer Translator Repository Management Tool Text + Meta

    Extractor Reflector ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 開発チームと翻訳チームの分業 - 144 -
  86. ◈ ブランチ (環境) ◈ Arg1: テキスト or シナリオ ◈ Arg2:

    未翻訳 or 翻訳済み ◈ Arg3: 新規・追加 or 修正差分 ◈ その他絞り込み |Slackワークフロー ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Slackワークフロー - 148 -
  87. ◈ メニューから「トリガー」を追加 ◈ 関数: Jenkinsに送信する関数 ◈ ソース: 「スプレッドシートから」 ◈ 種類:

    「変更時」 |Googleスプレッドシート&GoogleAppsScript設定 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Slackワークフロー - 151 -
  88. ◈ Generic Webhook Trigger Plugin ◈ 指定されたURLにPOST送信 ◈ ペイロードはVariableで指定した 変数に格納される

    ◈ pipelineでJSONデータをパース して任意のジョブを実行 |Jenkins設定 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Slackワークフロー - 152 -
  89. 開発チーム 翻訳チーム Developer Translator Repository Management Tool Text + Meta

    Extractor Reflector ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Slackワークフロー - 153 -
  90. ◈ 課題と目標 ◈ 設計 ◈ 実装 ◈ 開発チームと翻訳チームの分業 ◈ ローカライズで困ったこと

    ◈ まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 目次 - 155 -
  91. |複数翻訳パターン ◈ ひとつの日本語に別々の英訳 日本語: おすすめ (提案, 最適化) 英語 : Suggest,

    Optimize ◈ 利用場面に応じた表記 日本語 : こころの器 英語 : Heartphial タイトル表題: Heartphials ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ローカライズで困ったこと - 157 -
  92. |複数翻訳パターン ◈ 日本語版での英字表記の翻訳 日本語: バトル, Battle 英語 : Battle, Battle

    ◈ 短縮系 英語 : Attack, Defense 短縮系: ATK, DEF ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ローカライズで困ったこと - 158 -
  93. |複数翻訳パターン ◈ 日本語版での英字表記の翻訳 日本語: バトル, Battle 英語 : Battle, Battle

    Battler ◈ 短縮系 英語 : Attack, Defense 短縮系: ATK, DEF ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ローカライズで困ったこと - 159 -
  94. ◈ 課題と目標 ◈ 設計 ◈ 実装 ◈ 開発チームと翻訳チームの分業 ◈ ローカライズで困ったこと

    ◈ まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 目次 - 163 -
  95. ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ❖ 忙しすぎる開発チームの負担を増やしてはいけない ❖ テキストだけでは自然な翻訳は難しい

    ❖ 抜け漏れをなくすために実装ルールや制限を上手に使う ❖ 自動化するだけでは不十分、ユーザーフレンドリーを ❖ 同期は小さく頻繁に! ❖ 無理な「協力」より自律した上での「協創」を まとめ - 165 -
  96. WFSセッション一覧 WFSはCEDEC 2025で全8セッション登壇いたします ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ©WFS

    Developed by WRIGHT FLYER STUDIOS © VISUAL ARTS/Key 7/22 第4会場 13:40-14:40 レギュラーセッション LLM翻訳ツールの開発と 海外のお客様対応等への社内導入事例 郡司 匡弘 / 松井 望 / 小野 幸人 BP 7/23 第1会場 09:30-10:30 レギュラーセッション ヘブンバーンズレッドにおける、 世界観を活かしたミニゲーム企画の作り 方 菊岡 大夢 GD 7/23 第1会場 10:50-11:50 レギュラーセッション ヘブンバーンズレッドの レンダリングパイプライン刷新 野口 顕弘 ENG 7/23 第2会場 13:20-14:20 レギュラーセッション 「魔法少女まどか☆マギカ Magia Exedra」の グローバル展開を支える、開発チームと翻訳チームの 「意識しない協創」を実現するローカライズシステム 原田 大志 / 篠原 功 PRD 7/23 第2会場 13:20-14:20 レギュラーセッション 「魔法少女まどか☆マギカ Magia Exedra」 の必殺技演出を徹底解剖! -キャラクターの魅力を最大限にファンに届けるためのこだわり- 新谷 雄輝 / 金子 俊太朗 / 佐々木 文哉 VA 7/24 第5会場 15:00-15:25 ショートセッション ライブサービスゲームQAのパフォーマ ンス検証による品質改善の取り組み 小野 粋哉 / 勅使川原 大輔 BP 7/24 第5会場 18:00-18:25 ショートセッション ヒューリスティック評価を用いた ゲームQA実践事例 山本 幸寛 PRD 7/24 第8会場 18:00-18:25 ショートセッション 「魔法少女まどか☆マギカ Magia Exedra」での負荷試験の実践と学び 悦田 潤哉 ENG