Slide 1

Slide 1 text

©WFS

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

◈ 『魔法少女まどかマギカ』の世界を 3Dグラフィックで追体験! ◈ 歴代魔法少女が織りなすシンプルで奥 深いバトルシステム! ◈ 日本語・英語、20の国と地域で展開 |魔法少女まどか☆マギカ Magia Exedra ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS - 2 -

Slide 4

Slide 4 text

|セッションについて ◈ アプリを複数言語、同時リリースを実現する ためのローカライズシステムの紹介 ◈ 開発、翻訳チームの自律を支える自動化やツ ールなども紹介 ◈ 翻訳していて困ったことも実例で紹介 ◈ 講演資料は講演後にCEDiLにて公開します ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS - 3 -

Slide 5

Slide 5 text

|原田 大志 Masashi HARADA ◈ 2020年に株式会社ポケラボに入社、現在は 株式会社WFSに所属。 ◈ 新規プロダクトの開発をしながら、開発効 率化のためのツール制作など ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 自己紹介 - 4 -

Slide 6

Slide 6 text

|篠原 功 Isao SHINOHARA ◈ 2013年に株式会社ポケラボに入社、 現在は株式会社WFSに所属 ローカライズシステムのほか リソース管理システムなどを担当 ◈ 開発・運用: 「SINoALICE -シノアリス-」 「アサルトリリィ Last Bullet」 基盤: 「魔法少女まどか☆マギカ Magia Exedra」 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 自己紹介 - 5 -

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

課題と目標 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS - 13 -

Slide 15

Slide 15 text

|内容 ❖運営型ゲームの多言語対応の難しさ ❖目指したローカライズシステムの姿 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 課題と目標 - 14 -

Slide 16

Slide 16 text

|内容 ❖運営型ゲームの多言語対応の難しさ ❖目指したローカライズシステムの姿 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 課題と目標 - 15 -

Slide 17

Slide 17 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS アプリでやっていること 運営型ゲームの多言語対応の難しさ - 16 -

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 散在する膨大な翻訳対象を管理しきれない ✓ UIもデータもシナリオも対象は大量 ✓ 1日分の変更量だって把握しきれない ✓ 変更する人も大勢いる ✓ 追加、変更を一元管理するのは難しい 運営型ゲームの多言語対応の難しさ - 21 -

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS スケジュールのコントロールが難しい ✓ いつ、どれくらいの量がくるのか予測できない ✓ リリース日程にも影響がでてしまう ✓ リリースを分けても問題がある 運営型ゲームの多言語対応の難しさ - 23 -

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 継続して追加、変更が発生する ✓ 終わったと思っても変更は発生する ✓ 変更が入れば翻訳作業も発生する ✓ チーム間で擦り合わせるのが難しい 運営型ゲームの多言語対応の難しさ - 25 -

Slide 27

Slide 27 text

|内容 ❖運営型ゲームの多言語対応の難しさ ❖目指したローカライズシステムの姿 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 課題と目標 - 26 -

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 翻訳チームのタイミングで翻訳対象を取得できること 開発チーム Developer Extractor Repository Text + Meta リポジトリからその時点での最新 のデータを全て抽出する。 開発チームを介さずに自由に実行 できる。 目指したローカライズシステムの姿 - 28 -

Slide 30

Slide 30 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 追加、変更含めて全体像が把握できること 開発チーム 翻訳チーム Developer Translator Extractor Repository Management Tool Text + Meta 抽出データを翻訳チームの管理システ ムに取り込む。 日本語をキーにして管理することで重 複は省かれ、未登録の日本語だけが新 規として取り込まれる。 翻訳テキスト以外のメタ情報と合わせ て翻訳を行う。 目指したローカライズシステムの姿 - 29 -

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 抽出機と反映機 開発チーム 翻訳チーム Developer Translator Repository Management Tool Text + Meta Extractor Reflector 目指したローカライズシステムの姿 - 31 -

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

設計 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS - 33 -

Slide 35

Slide 35 text

|内容 ❖どうやって元の位置に反映するか? ❖メタ情報の取り扱い ❖翻訳テキストの納品 ❖動的反映と静的反映 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 設計 - 34 -

Slide 36

Slide 36 text

|内容 ❖どうやって元の位置に反映するか? ❖メタ情報の取り扱い ❖翻訳テキストの納品 ❖動的反映と静的反映 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 設計 - 35 -

Slide 37

Slide 37 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS どうやって元の位置に反映するか? どこに戻せば いいんだろう? Fate Weave 設計 - 36 -

Slide 38

Slide 38 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS どうやって元の位置に反映するか? 場所は違ってもテキストの変化は 一緒。 「編成」をキーにして 「Assign」が取得できればいい。 翻訳テーブル Japanese English ユニオン Union 編成 Assign パーティ Party 設計 - 37 -

Slide 39

Slide 39 text

|内容 ❖どうやって元の位置に反映するか? ❖メタ情報の取り扱い ❖翻訳テキストの納品 ❖動的反映と静的反映 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 設計 - 38 -

Slide 40

Slide 40 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS メタ情報の取り扱い 設計 - 39 -

Slide 41

Slide 41 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS メタ情報の取り扱い 設計 - 40 -

Slide 42

Slide 42 text

|内容 ❖どうやって元の位置に反映するか? ❖メタ情報の取り扱い ❖翻訳テキストの納品 ❖動的反映と静的反映 設計 - 41 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS

Slide 43

Slide 43 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 翻訳テキストの納品 設計 - 42 -

Slide 44

Slide 44 text

|内容 ❖どうやって元の位置に反映するか? ❖メタ情報の取り扱い ❖翻訳テキストの納品 ❖動的反映と静的反映 設計 - 43 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS

Slide 45

Slide 45 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 動的反映と静的反映 Japanese English ユニオン Union パーティ Party クエスト Quests 翻訳テーブル 画面の初期化時に、各テキストコンポーネント自身が、 翻訳テーブルから取得する。 埋め込みなどの準備は必要ない。 設計 - 44 -

Slide 46

Slide 46 text

| ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 日本語字幕を元に、翻訳テーブルを使 って英語用の字幕ファイルを生成。 動的反映と静的反映 英語 + + 日本語 動画は共通 設計 - 45 -

Slide 47

Slide 47 text

©WFS ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS - 46 -

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

実装 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS - 48 -

Slide 50

Slide 50 text

|実装方針 ◈ 開発者に負担が少ないこと ◈ 抽出、表示・反映に抜け漏れが発生しないこと 上記が満たせない実装方法は禁止!! 実装 - 49 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS

Slide 51

Slide 51 text

|使用技術 ◈ クライアント: Unity, C# ◈ サーバー: PHP, YAML ◈ DB: Spanner ◈ ストリーミング動画: HLS 実装 - 50 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 53 -

Slide 55

Slide 55 text

◈ 翻訳用のコンポーネントを用意 ◈ TextMeshProがアタッチされて いるGameObjectにアタッチ |実装ルール ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 54 -

Slide 56

Slide 56 text

◈ AssetDatabase.FindAssets("t:Scene t:Prefab"); でシーンやプレハブのアセットを取得 ◈ テキストコンポーネントから翻訳対象となる 日本語テキストを抽出 |抽出方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 55 -

Slide 57

Slide 57 text

©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(true)) { // Unityシーン: TextMeshProに入力された日本語テキストをリストに追加 texts.Add(component.text); } } EditorSceneManager.CloseScene(scene, true); } else if (asset is GameObject prefab) { foreach (var component in prefab.GetComponentsInChildren(true)) { // Unityプレハブ: TextMeshProに入力された日本語テキストリストに追加 texts.Add(component.text); } } } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 56 -

Slide 58

Slide 58 text

©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(true)) { // Unityシーン: TextMeshProに入力された日本語テキストをリストに追加 texts.Add(component.text); } } EditorSceneManager.CloseScene(scene, true); } else if (asset is GameObject prefab) { foreach (var component in prefab.GetComponentsInChildren(true)) { // Unityプレハブ: TextMeshProに入力された日本語テキストリストに追加 texts.Add(component.text); } } } ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 57 -

Slide 59

Slide 59 text

©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 -

Slide 60

Slide 60 text

©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 -

Slide 61

Slide 61 text

©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 -

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

◈ 翻訳データを事前にダウンロード ◈ GameObjectが表示されるタイミングで動的に翻訳 ◈ コンポーネントから取得した日本語テキストから ハッシュ値を算出して対応した英語テキストに置き換え |表示・反映方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 63 -

Slide 65

Slide 65 text

©WFS public class LocalizeTextComponent : MonoBehaviour { private void OnEnable() { var component = GetComponent(); // 元の日本語テキストをハッシュ化して英語テキストを取得 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 -

Slide 66

Slide 66 text

|表示・反映 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 65 -

Slide 67

Slide 67 text

◈ 変数を定義してインスペクター から入力する方法は使用禁止 ◈ 理由 翻訳対象となる日本語テキスト を抽出するのが難しい 翻訳処理を通しているかの 保証ができない |使用禁止ルール: 変数定義によるテキスト利用 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Unity UIテキストコンポーネント - 66 -

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 68 -

Slide 70

Slide 70 text

◈ 決められたクラス内に定義 ◈ プロパティ形式にして 翻訳メソッドを通す ◈ 決められたクラス以外で 定義することは禁止 |実装ルール ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 69 -

Slide 71

Slide 71 text

◈ 日本語テキストが定義されているクラスの プロパティ情報を取得 ◈ GetValue()を実行して日本語テキストを抽出 |抽出方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 70 -

Slide 72

Slide 72 text

©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 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 -

Slide 73

Slide 73 text

©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 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 -

Slide 74

Slide 74 text

©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 -

Slide 75

Slide 75 text

©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 -

Slide 76

Slide 76 text

©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 -

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

◈ 翻訳データを事前にダウンロード ◈ プロパティ利用時に動的翻訳 |表示・反映方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 78 -

Slide 80

Slide 80 text

©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 -

Slide 81

Slide 81 text

©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 -

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

|表示・反映方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS C#ソースコード - 86 -

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 88 -

Slide 90

Slide 90 text

◈ シナリオはGoogle スプレッドシートで作成 ◈ UnityEditorでインポート ◈ ScriptableObjectの アセット形式で管理 |シナリオデータ管理 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 89 -

Slide 91

Slide 91 text

◈ なし ScriptableObject内のセリフと 話者名の抽出が可能だった |実装ルール 抽出、表示・反映のための 特別な実装の必要はなし ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 90 -

Slide 92

Slide 92 text

◈ AssetDatabase.FindAssets($"t:{typeof(Class)}"); でシナリオ(ScriptableObject)のアセットを取得 ◈ シナリオ(ScriptableObject)に含まれる セリフ、話者名を抽出 |抽出方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 91 -

Slide 93

Slide 93 text

©WFS var texts = new List(); 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 -

Slide 94

Slide 94 text

©WFS var texts = new List(); 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 -

Slide 95

Slide 95 text

◈ シナリオ(ScriptableObject)に含まれる セリフ、話者名を英語テキストに置き換え ◈ JSON形式に変換して言語別にファイルを保存 ◈ 言語に応じたファイルをダウンロード |表示・反映方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 94 -

Slide 96

Slide 96 text

©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 -

Slide 97

Slide 97 text

©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 -

Slide 98

Slide 98 text

©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 -

Slide 99

Slide 99 text

©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 -

Slide 100

Slide 100 text

©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 -

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

|表示・反映方法 scenario0001.json scenario0001_en-Latn.json ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS シナリオデータ(ScriptableObject) - 103 -

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ストリーミング動画の字幕 - 105 -

Slide 107

Slide 107 text

◈ File Signature ◈ 空行 ◈ キュータイミング ◈ 字幕テキスト |HLS .vttファイル (WebVTTフォーマット) ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ストリーミング動画の字幕 - 106 -

Slide 108

Slide 108 text

◈ なし 字幕ファイルのフォーマットから テキスト部分の抽出が可能 |実装ルール 抽出、表示・反映のための 特別な実装の必要はなし ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ストリーミング動画の字幕 - 107 -

Slide 109

Slide 109 text

◈ .vttファイルをロード ◈ 「File Signature」「空行」「キュータイミング」 を除いた「字幕テキスト」部分を抽出 |抽出方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ストリーミング動画の字幕 - 108 -

Slide 110

Slide 110 text

©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 -

Slide 111

Slide 111 text

©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 -

Slide 112

Slide 112 text

◈ .vttファイルをロード ◈ 字幕テキストを英語テキストに置き換え ◈ 置き換えた.vttファイルを別ファイルに保存 |表示・反映方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ストリーミング動画の字幕 - 111 -

Slide 113

Slide 113 text

©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 -

Slide 114

Slide 114 text

©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 -

Slide 115

Slide 115 text

|表示・反映方法 movie0001.vtt movie0001_en-Latn.vtt ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ストリーミング動画の字幕 - 114 -

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS マスターデータ - 116 -

Slide 118

Slide 118 text

◈ なし Spannerのスキーマ情報から 日本語テキストを抽出可能 |実装ルール 抽出、表示・反映のための 特別な実装の必要はなし ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS マスターデータ - 117 -

Slide 119

Slide 119 text

◈ テーブルのスキーマ情報を取得 ◈ STRING(XXX)で定義されているカラムの値を抽出 |抽出方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS マスターデータ - 118 -

Slide 120

Slide 120 text

©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 -

Slide 121

Slide 121 text

©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 -

Slide 122

Slide 122 text

©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 -

Slide 123

Slide 123 text

©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 -

Slide 124

Slide 124 text

©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 -

Slide 125

Slide 125 text

◈ 日本語マスターデータを取得 ◈ 日本語テキストを英語テキストに置き換え ◈ 英語用のテーブルに反映 |表示・反映方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS マスターデータ - 124 -

Slide 126

Slide 126 text

©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 -

Slide 127

Slide 127 text

©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 -

Slide 128

Slide 128 text

|表示・反映方法 table: SkillMst table: SkillMst_en_Latn ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS マスターデータ - 127 -

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 設定ファイル(YAML) - 129 -

Slide 131

Slide 131 text

◈ なし 「ひらがな、カタカナ、漢字」だ った場合に抽出 |実装ルール 抽出、表示・反映のための 特別な実装の必要はなし ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 設定ファイル(YAML) - 130 -

Slide 132

Slide 132 text

◈ 正規表現で「ひらがな」「カタカナ」「漢字」が 含まれているかどうかを判定 ◈ 含まれている場合は翻訳対象として抽出 |抽出方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 設定ファイル(YAML) - 131 -

Slide 133

Slide 133 text

©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 -

Slide 134

Slide 134 text

©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 -

Slide 135

Slide 135 text

◈ yamlデータ取得時に翻訳を動的に実行 ◈ yamlデータに「ひらがな」「カタカナ」「漢字」が 含まれている場合は英語テキストに置き換え |表示・反映方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 設定ファイル(YAML) - 134 -

Slide 136

Slide 136 text

©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 -

Slide 137

Slide 137 text

©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 -

Slide 138

Slide 138 text

|表示・反映方法 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 設定ファイル(YAML) - 137 -

Slide 139

Slide 139 text

◈ TextMeshProがアタッチされているGameObjectに 翻訳用のコンポーネントをアタッチ ◈ C#のソースコード上で日本語テキストを使いたい 場合は決められたクラス内に定義 ◈ 決められたルール以外での実装は行わない |実装まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 実装 - 138 -

Slide 140

Slide 140 text

◈ TextMeshProがアタッチされているGameObjectに 翻訳用のコンポーネントをアタッチ ◈ C#のソースコード上で日本語テキストを使いたい 場合は決められたクラス内に定義 ◈ 決められたルール以外での実装は行わない |実装まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 実装 - 139 -

Slide 141

Slide 141 text

◈ TextMeshProがアタッチされているGameObjectに 翻訳用のコンポーネントをアタッチ ◈ C#のソースコード上で日本語テキストを使いたい 場合は決められたクラス内に定義 ◈ 決められたルール以外での実装は行わない |実装まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 実装 - 140 -

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

開発チームと翻訳チームの分業 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS - 142 -

Slide 144

Slide 144 text

|使用技術 ◈ Googleスプレッドシート ◈ Google Apps Script ◈ Jenkins ◈ Slack 開発チームと翻訳チームの分業 - 143 - ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

◈ スラッシュ( / )から始まる特定のコマンドを入力、 送信を行うことでアプリを呼び出すことが可能 ◈ 引数を指定してアプリに渡すこともできる |Slack Slash Commandsとは ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Slack Slash Commands - 145 -

Slide 147

Slide 147 text

◈ 非エンジニアには馴染みのないコマンド ◈ コマンド実行時の引数指定が複雑 |Slack Slash Commandsの課題点 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Slack Slash Commands - 146 -

Slide 148

Slide 148 text

◈ 定型作業を自動化 ◈ 様々なイベントトリガーから 起動可能 ◈ ステップを繋げて作業を完遂 |Slackワークフローとは ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Slackワークフロー - 147 -

Slide 149

Slide 149 text

◈ ブランチ (環境) ◈ Arg1: テキスト or シナリオ ◈ Arg2: 未翻訳 or 翻訳済み ◈ Arg3: 新規・追加 or 修正差分 ◈ その他絞り込み |Slackワークフロー ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Slackワークフロー - 148 -

Slide 150

Slide 150 text

©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 開発チームと翻訳チームの分業 - 149 -

Slide 151

Slide 151 text

◈ ステップ1: 「情報をフォームで収集する」 ◈ ステップ2: 「スプレッドシートに追加する」 ◈ ステップ3: 「チャンネルへメッセージを 送信する」 |Slackワークフロー設定 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Slackワークフロー - 150 -

Slide 152

Slide 152 text

◈ メニューから「トリガー」を追加 ◈ 関数: Jenkinsに送信する関数 ◈ ソース: 「スプレッドシートから」 ◈ 種類: 「変更時」 |Googleスプレッドシート&GoogleAppsScript設定 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Slackワークフロー - 151 -

Slide 153

Slide 153 text

◈ Generic Webhook Trigger Plugin ◈ 指定されたURLにPOST送信 ◈ ペイロードはVariableで指定した 変数に格納される ◈ pipelineでJSONデータをパース して任意のジョブを実行 |Jenkins設定 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Slackワークフロー - 152 -

Slide 154

Slide 154 text

開発チーム 翻訳チーム Developer Translator Repository Management Tool Text + Meta Extractor Reflector ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS Slackワークフロー - 153 -

Slide 155

Slide 155 text

◈ Slackワークフローを導入したことで 大半の作業が翻訳チームのみで完結 ◈ 翻訳依頼や反映など作業に関するやり取りや 相手の対応待ちといった時間を大幅に削減 ◈ 自分達の作業に集中できる時間が増加し品質が向上 |開発チームと翻訳チームの分業まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS 開発チームと翻訳チームの分業 - 154 -

Slide 156

Slide 156 text

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

Slide 157

Slide 157 text

ローカライズで困ったこと ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS - 156 -

Slide 158

Slide 158 text

|複数翻訳パターン ◈ ひとつの日本語に別々の英訳 日本語: おすすめ (提案, 最適化) 英語 : Suggest, Optimize ◈ 利用場面に応じた表記 日本語 : こころの器 英語 : Heartphial タイトル表題: Heartphials ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ローカライズで困ったこと - 157 -

Slide 159

Slide 159 text

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

Slide 160

Slide 160 text

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

Slide 161

Slide 161 text

|ダミーテキスト ◈ 無駄な翻訳作業が発生 ◈ 用途不明なテキストの混入 Unity UIコンポーネントで多数発生 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ローカライズで困ったこと - 160 -

Slide 162

Slide 162 text

|ダミーテキスト [解決案] ◈ ダミーだとわかるテキストを利用 先頭に「仮」をつけるなど ◈ 実装者の負担にもなるので実現には プロダクトの理解が必要 ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ローカライズで困ったこと - 161 -

Slide 163

Slide 163 text

|UI文字数オーバー ◈ 基本的に英語のほうが文字数が多い ◈ 2〜5文字程度の熟語も英語にすると 5〜10倍の文字数になることも ◈ 日本語は主語や目的語を省くことも 多いが英語では明記する必要がある ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ローカライズで困ったこと - 162 -

Slide 164

Slide 164 text

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

Slide 165

Slide 165 text

まとめ ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS - 164 -

Slide 166

Slide 166 text

©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS ❖ 忙しすぎる開発チームの負担を増やしてはいけない ❖ テキストだけでは自然な翻訳は難しい ❖ 抜け漏れをなくすために実装ルールや制限を上手に使う ❖ 自動化するだけでは不十分、ユーザーフレンドリーを ❖ 同期は小さく頻繁に! ❖ 無理な「協力」より自律した上での「協創」を まとめ - 165 -

Slide 167

Slide 167 text

ご清聴ありがとうございました ©2024 Magica Quartet/Aniplex,Magia Exedra Project ©WFS

Slide 168

Slide 168 text

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

Slide 169

Slide 169 text

©WFS