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]" 반환)
}
}
}