Files
EnglewoodLAB/Assets/Scripts/UVC/Locale/LocalizationManager.cs

397 lines
18 KiB
C#
Raw Permalink Normal View History

2026-01-20 11:27:56 +09:00
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using Unity.VisualScripting;
using UnityEngine;
using UVC.Json;
using UVC.Log;
namespace UVC.Locale
{
/// <summary>
/// 다국어 데이터를 관리하고, 현재 설정된 언어에 맞는 번역 문자열을 제공하는 클래스입니다.
/// JSON 파일을 로드하여 다국어 데이터를 초기화하며, 언어 변경 시 이벤트를 통해 알림을 제공합니다.
/// 이 클래스는 싱글톤으로 구현되어 어디서든 <see cref="Instance"/>를 통해 접근할 수 있습니다.
/// </summary>
/// <example>
/// <code>
/// // 1. LocalizationManager 인스턴스 접근 (싱글톤)
/// // LocalizationManager locManager = LocalizationManager.Instance; // 직접 인스턴스화 대신 Instance 사용
///
/// // 2. (선택 사항) 언어 변경 이벤트 구독
/// LocalizationManager.Instance.OnLanguageChanged += (newLanguage) => {
/// ULog.Debug($"언어가 {newLanguage}(으)로 변경되었습니다.");
/// // UI 업데이트 로직 등
/// };
///
/// // 3. 다국어 데이터 로드 (StreamingAssets 폴더의 locale.json 파일을 기본으로 로드)
/// // 인스턴스 생성 시 자동으로 로드하거나, 별도의 초기화 메서드를 통해 로드할 수 있습니다.
/// // 예시: 로드 메서드가 private 생성자 또는 별도 초기화 메서드에서 호출된다고 가정
/// if (LocalizationManager.Instance.LoadDefaultLocalizationData()) // LoadDefaultLocalizationData가 public이라고 가정
/// {
/// ULog.Debug("다국어 데이터 로드 성공!");
///
/// // 4. 사용 가능한 언어 목록 확인 (언어 코드 기준)
/// List<string> availableLangCodes = LocalizationManager.Instance.AvailableLanguages();
/// ULog.Debug("사용 가능한 언어 코드: " + string.Join(", ", availableLangCodes));
///
/// // 특정 언어 코드의 표시 이름 가져오기
/// string koreanDisplayName = LocalizationManager.Instance.GetLanguageDisplayName("ko");
/// ULog.Debug($"ko display name: {koreanDisplayName}"); // 출력: 한국어
///
/// // 5. 언어 변경
/// LocalizationManager.Instance.SetCurrentLanguage("ko"); // 한국어로 변경
///
/// // 6. 번역된 문자열 가져오기
/// string errorMsg = LocalizationManager.Instance.GetString("error");
/// ULog.Debug(errorMsg); // 현재 언어가 "ko"이면 "에러" 출력
///
/// LocalizationManager.Instance.SetCurrentLanguage("en"); // 영어로 변경
/// welcomeMsg = LocalizationManager.Instance.GetString("welcome_message");
/// ULog.Debug(welcomeMsg); // 출력 (영어): Welcome!
/// }
/// else
/// {
/// ULog.Error("다국어 데이터 로드 실패.");
/// }
/// </code>
/// </example>
public class LocalizationManager
{
#region singleton
// 싱글톤 인스턴스를 저장하기 위한 private static 변수
private static LocalizationManager _instance;
// 멀티스레드 환경에서의 동시 접근을 제어하기 위한 lock 객체
private static readonly object _lock = new object();
/// <summary>
/// <see cref="LocalizationManager"/>의 싱글톤 인스턴스를 가져옵니다.
/// 인스턴스가 아직 생성되지 않았다면 새로 생성하여 반환합니다.
/// </summary>
public static LocalizationManager Instance
{
get
{
// Double-checked locking으로 스레드 안전성 확보
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new LocalizationManager();
//_instance.LoadDefaultLocalizationData("ko"); // 기본 언어 데이터 로드
}
}
}
return _instance;
}
}
#endregion
private LocalizationDataSource _dataSource;
private string _currentLanguage;
/// <summary>
/// 언어가 변경되었을 때 발생하는 이벤트의 델리게이트입니다.
/// </summary>
/// <param name="newLanguageCode">새롭게 설정된 언어 코드입니다 (예: "en", "ko").</param>
public delegate void LanguageChangedHandler(string newLanguageCode);
/// <summary>
/// 현재 언어가 <see cref="SetCurrentLanguage"/> 메서드를 통해 성공적으로 변경되었을 때 발생하는 이벤트입니다.
/// </summary>
public event LanguageChangedHandler OnLanguageChanged;
2026-03-10 11:35:30 +09:00
/// <summary>
/// <see cref="LoadDefaultLocalizationData"/>에서 모든 데이터 로드가 완료되었을 때 한 번 발생하는 이벤트입니다.
/// 이미 로드가 완료된 후 구독하면 즉시 호출됩니다.
/// </summary>
public event Action? OnReady
{
add
{
OnReadyInternal += value;
if (IsReady) value?.Invoke();
}
remove => OnReadyInternal -= value;
}
private event Action? OnReadyInternal;
/// <summary>다국어 데이터 로드가 완료되었는지 여부를 나타냅니다.</summary>
public bool IsReady { get; private set; }
2026-01-20 11:27:56 +09:00
/// <summary>
/// 현재 설정된 언어 코드를 가져옵니다.
/// </summary>
public string CurrentLanguage => _currentLanguage;
/// <summary>
/// 사용 가능한 언어 목록에서 현재 언어의 인덱스를 가져옵니다.
/// </summary>
public int CurrentLanguageIndex
{
get
{
if (_currentLanguage == null)
{
ULog.Warning("Current language is not set. Returning -1.");
return -1; // 언어가 설정되지 않은 경우 -1 반환
}
if (_dataSource?.Languages == null || _dataSource.Languages.Count == 0)
{
ULog.Warning("Languages data source is not loaded or empty. Returning -1.");
return -1; // 언어 데이터가 로드되지 않았거나 비어있는 경우 -1 반환
}
// AvailableLanguages를 통해 현재 언어의 인덱스를 찾습니다.
List<string> availableLanguages = AvailableLanguages;
int index = availableLanguages.IndexOf(_currentLanguage);
if (index != -1)
{
return index; // 현재 언어가 AvailableLanguages에 존재하는 경우 인덱스 반환
}
else
{
ULog.Warning($"Current language '{_currentLanguage}' not found in available languages. Returning -1.");
return -1; // 현재 언어가 AvailableLanguages에 없는 경우 -1 반환
}
}
}
/// <summary>
/// 로드된 다국어 데이터에서 사용 가능한 모든 언어 코드 목록을 가져옵니다.
/// 데이터가 로드되지 않았거나 번역 데이터가 비어있으면 빈 리스트를 반환합니다.
/// </summary>
public List<string> AvailableLanguages
{
get
{
if (_dataSource?.Languages != null && _dataSource.Languages.Count > 0)
{
return new List<string>(_dataSource.Languages.Keys);
}
return new List<string>();
}
}
/// <summary>
/// 사용 가능한 표시 언어 컬렉션을 가져옵니다.
/// ["en"] = "English", ["ko"] = "한국어" 형식의 KeyValuePair 컬렉션입니다.
/// </summary>
public Dictionary<string, string> AvailableDisplayLanguages
{
get
{
if (_dataSource?.Languages != null && _dataSource.Languages.Count > 0)
{
return new Dictionary<string, string>(_dataSource.Languages.AsReadOnlyCollection());
}
return new Dictionary<string, string>();
}
}
/// <summary>
/// 지정된 언어 코드에 해당하는 표시 이름을 가져옵니다.
/// </summary>
/// <param name="languageCode">표시 이름을 가져올 언어 코드입니다.</param>
/// <returns>언어 코드에 해당하는 표시 이름입니다. 해당 코드가 없으면 null을 반환합니다.</returns>
public string GetLanguageDisplayName(string languageCode)
{
if (string.IsNullOrEmpty(languageCode) || _dataSource?.Languages == null)
{
return null;
}
_dataSource.Languages.TryGetValue(languageCode, out string displayName);
return displayName;
}
/// <summary>
/// <see cref="LocalizationManager"/> 클래스의 새 인스턴스를 초기화합니다.
/// 기본 언어를 "ko"로 설정합니다.
/// 생성자를 private으로 변경하여 외부에서의 직접적인 인스턴스 생성을 막습니다.
/// </summary>
private LocalizationManager()
{
_dataSource = new LocalizationDataSource();
}
/// <summary>
/// 기본 경로(<see cref="Application.streamingAssetsPath"/>/locale.json)에서 다국어 데이터를 로드합니다.
/// </summary>
/// <returns>데이터 로드 성공 시 true, 실패 시 false를 반환합니다.</returns>
public bool LoadDefaultLocalizationData(string defaultLanguage = "ko", string? customPath = null)
{
// 기본 언어를 설정하거나, 시스템 설정을 따르도록 초기화할 수 있습니다.
_currentLanguage = defaultLanguage; // 예시 기본 언어
string path = Path.Combine(Application.streamingAssetsPath, customPath ?? "locale.json");
2026-03-10 11:35:30 +09:00
bool result = LoadLocalizationData(path);
if (result)
{
IsReady = true;
OnReadyInternal?.Invoke();
}
return result;
2026-01-20 11:27:56 +09:00
}
/// <summary>
/// 지정된 파일 경로에서 JSON 형식의 다국어 데이터를 로드합니다.
/// </summary>
/// <param name="filePath">로드할 JSON 파일의 전체 경로입니다.</param>
/// <returns>데이터 로드 및 분석 성공 시 true, 실패 시 false를 반환합니다.</returns>
public bool LoadLocalizationData(string filePath)
{
if (!File.Exists(filePath))
{
ULog.Error($"Localization file not found: {filePath}", new System.Exception($"Localization file not found: {filePath}"));
return false;
}
try
{
string jsonData = File.ReadAllText(filePath);
// 직접 Dictionary로 역직렬화한 후 Translations에 할당
var loadedDataSource = JsonHelper.FromJson<LocalizationDataSource>(jsonData);
if (loadedDataSource == null || loadedDataSource.Words == null || loadedDataSource.Languages == null)
{
ULog.Error($"Failed to deserialize localization data, or 'languages'/'words' section is missing in: {filePath}");
_dataSource = new LocalizationDataSource(); // 빈 데이터 소스로 초기화
return false;
}
_dataSource = loadedDataSource;
// 데이터 로드 후, 현재 설정된 언어가 유효한지 확인하고,
// 유효하지 않다면 사용 가능한 첫 번째 언어나 기본 언어로 설정할 수 있습니다.
List<string> availableLangCodes = AvailableLanguages;
if (!string.IsNullOrEmpty(_currentLanguage) && !availableLangCodes.Contains(_currentLanguage))
{
ULog.Warning($"Current language '{_currentLanguage}' not found in the new data source's 'languages' section. Attempting to set to default or first available.");
if (availableLangCodes.Count > 0)
{
SetCurrentLanguage(availableLangCodes[0]);
}
}
return true;
}
catch (Exception ex)
{
ULog.Error($"Error loading localization data from {filePath}: {ex.Message}", ex);
_dataSource = new LocalizationDataSource(); // 오류 발생 시 빈 데이터 소스로 초기화
return false;
}
}
/// <summary>
/// 현재 사용 언어를 변경합니다.
/// 성공적으로 언어가 변경되면 <see cref="OnLanguageChanged"/> 이벤트가 발생합니다.
/// </summary>
/// <param name="languageCode">설정할 언어 코드입니다 (예: "en", "ko").</param>
public void SetCurrentLanguage(string languageCode)
{
if (string.IsNullOrEmpty(languageCode))
{
ULog.Warning("Language code cannot be null or empty.");
return;
}
if (_dataSource?.Languages == null || _dataSource.Languages.Count == 0)
{
ULog.Warning("Languages data source is not loaded or empty. Cannot set language.");
return;
}
//AvailableLanguages를 통해 해당 언어가 지원되는지 확인합니다.
if (_dataSource.Languages.ContainsKey(languageCode))
{
if (_currentLanguage != languageCode)
{
_currentLanguage = languageCode;
OnLanguageChanged?.Invoke(_currentLanguage);
ULog.Debug($"Current language changed to: {languageCode} ({GetLanguageDisplayName(languageCode)})");
}
}
else
{
ULog.Warning($"Language code '{languageCode}' not found in 'languages' data source. Current language remains '{_currentLanguage}'.");
}
}
/// <summary>
/// 현재 설정된 언어를 기준으로, 주어진 키에 해당하는 번역된 문자열을 가져옵니다.
/// </summary>
/// <param name="key">번역 문자열을 찾기 위한 키입니다.</param>
/// <returns>
/// 번역된 문자열을 반환합니다.
/// 키가 null이거나 비어있으면 빈 문자열을 반환합니다.
/// 해당 키 또는 현재 언어에 대한 번역을 찾을 수 없으면, "[키]" 형식의 문자열을 반환합니다.
/// </returns>
public string GetString(string key)
{
if (string.IsNullOrEmpty(key))
{
ULog.Warning("Requested localization key is null or empty.");
return string.Empty;
}
if (_dataSource?.Words == null)
{
ULog.Warning("Words data source is not loaded. Cannot get string for key: " + key);
return key;
}
if (_dataSource.Words.TryGetValue(key, out var languageSpecificStrings) &&
languageSpecificStrings.TryGetValue(_currentLanguage, out var translatedString))
{
return translatedString;
}
//ULog.Debug($"Translation for key '{key}' not found in language '{_currentLanguage}'.");
return key; // 키를 그대로 반환
}
/// <summary>
/// 특정 언어의 특정 키에 대한 번역된 문자열을 직접 가져옵니다.
/// </summary>
/// <param name="languageCode">문자열을 가져올 대상 언어 코드입니다.</param>
/// <param name="key">번역 문자열을 찾기 위한 키입니다.</param>
/// <returns>
/// 번역된 문자열을 반환합니다.
/// 언어 코드나 키가 null이거나 비어있으면 빈 문자열을 반환합니다.
/// 해당 언어 또는 키에 대한 번역을 찾을 수 없으면 null을 반환합니다.
/// </returns>
public string GetString(string languageCode, string key)
{
if (string.IsNullOrEmpty(languageCode))
{
ULog.Warning("Requested language code is null or empty for GetString(languageCode, key).");
return key;
}
if (string.IsNullOrEmpty(key))
{
ULog.Warning("Requested localization key is null or empty for GetString(languageCode, key).");
return key;
}
if (_dataSource?.Words == null)
{
ULog.Warning("Words data source is not loaded. Cannot get string for key: " + key + " in language: " + languageCode);
return key;
}
if (_dataSource.Words.TryGetValue(key, out var languageSpecificStrings) &&
languageSpecificStrings.TryGetValue(languageCode, out var translatedString))
{
return translatedString;
}
//ULog.Debug($"Translation for key '{key}' not found in language '{languageCode}' within 'words' data.");
return key; // 해당 언어/키가 없음을 표시 (이전에는 "[languageCode:key]" 반환)
}
}
}