포맷에 맞게 로컬라이징하기

# 학습 과정 소개

로컬라이징을 할 때, 번역할 텍스트에 상황에 따라 변할 수 있는 문자열이 포함될 수도 있습니다. 이럴 때 활용할 수 있는 함수를 알아보고 예제를 통해 그 사용법을 살펴보도록 합시다. 본 과정을 진행하기 전에 [로컬라이징의 이해](/docs?postId=951) 가이드를 먼저 살펴보면 도움이 됩니다.

# 포맷에 맞게 변환하기

번역할 텍스트에는 월드 상황에 따라 변할 수 있는 문자열이 포함될 수 있습니다. 예를 들어, 단순하게 한국어 "리나"를 영어 "Rina"로 번역하는 것이 아니라 "안녕, {0}!"라는 문장(포맷)을 번역하는 상황을 생각해 봅시다. 그리고 {0} 빈칸에는 "리나(Rina)", "스탄(Stan)", "알렉스(Alex)" 등 여러 NPC의 이름이 들어갈 수 있다고 가정해 봅시다. "안녕, {0}!"처럼 특정한 포맷이 있고, 이 문장의 빈칸을 상황에 따라 적절한 단어로 채워서 문자열을 출력하는 것을 "포맷에 맞게 변환한다."고 표현할 수 있습니다.   
포맷은 인수 빈칸(Argument hole)을 갖고 있습니다. 위의 예시 문장에서 {0} 부분이 바로 Argument hole입니다. [png](167480440826756abe6fbe7164b5588aa34f246d61f4f) 포맷의 한 문자열 안에는 여러 개의 Argument hole이 존재할 수 있습니다.   
포맷에 맞게 변환할 때는 `GetTextFormat()` 또는 `SmartFormat()` 함수를 활용합니다. 각 함수의 활용 방법을 자세히 살펴봅시다.

## GetTextFormat

`GetTextFormat()` 함수는 LocalizationTable에서 Key를 인수로 받습니다. 이 Key에 해당하는 텍스트를 찾고, 추가로 전달 받은 포맷 인수(format argument)가 있다면 그것을 형식에 맞게 채워 넣은 결과를 반환합니다.

예를 들어 아래와 같이 NPC 2명이 서 있고, 플레이어 캐릭터가 각 NPC와 만나면(충돌하면) 주황색 텍스트 상자에 "안녕, NPCName!"라는 문장을 출력해봅시다. [png](1674106811875c0dbb4e97c5a4c7595813713f1ed11ce)   


  1. 맵에 NPC 2명을 배치하고 각 NPC 엔티티의 이름을 아래와 같이 수정합니다. 

[{"NPC":"[png](1674097464492fb3054921620425a80664d1380f3f695)","Entity Name":"Stan"},{"NPC":"[png](16740974755475f8d0c294b5a491f907eefb8acc7bad1)","Entity Name":"Rina"}]

  

  2. NPC Stan과 Rina에 TriggerComponent를 추가합니다.   

  3. UI 에디터에서 ui - DefaultGroup에 텍스트 엔티티를 추가하고 이름을 TextBox로 설정합니다. 그리고 각 프로퍼티를 아래와 같이 설정합니다. 

[{"프로퍼티":"ImageRUID","값":"b9ab8455d5737bf4cb21433f00fdc19d"},{"프로퍼티":"Color","값":"[png](16752124711410bee46bc546d4bc68fd23e8476ca817e)"}]

  

  4. LocalizationTable을 생성하고 아래와 같이 입력합니다. 

[{"ID":"1","Key":"UI_TEXT_NPC_Stan","Source":"Stan","ko":"스탄","en":"Stan"},{"ID":"2","Key":"UI_TEXT_NPC_Rina","Source":"Rina","ko":"리나","en":"Rina"},{"ID":"3","Key":"UI_TEXT_HELLO","Source":"Hello, {0}!","ko":"안녕, {0}!","en":"Hello, {0}!"}]

  

  5. 새 스크립트 컴포넌트 LocalizationTest를 생성합니다. 그리고 Workspace - DefaultPlayer에 LocalizationTest 컴포넌트를 추가합니다.   

  6. LocalizationTest에 아래와 같이 프로퍼티를 추가합니다. 
         
         @Component
         script CharacterReferences extends Component
         property Entity Stan = "/maps/map01/Stan"
         property Entity Rina = "/maps/map01/Rina"
         end
         



    
    
          
    
    7. `OnBeginPlay()` 함수를 추가하고 아래와 같이 작성합니다. 실행 공간은 **client only**로 설정합니다.
        ```lua
    @Component
    script LocalizedNPCNameInitializer extends Component
        @ExecSpace("ClientOnly")
        method void OnBeginPlay()
        self.Stan.NameTagComponent.Name = _LocalizationService:GetText("UI_TEXT_NPC_Stan")
        self.Rina.NameTagComponent.Name = _LocalizationService:GetText("UI_TEXT_NPC_Rina")
        end
    end
    
    
    
      
    
    

  8. 이벤트 핸들러에 TriggerEnterEvent를 추가합니다. 실행 공간은 client only로 설정합니다. 그리고 아래와 같이 내용을 작성합니다. 
         
         @Component
         script TriggerGreetingDisplay extends Component
         @ExecSpace("ClientOnly")
         @EventSender("Self")
         handler HandleTriggerEnterEvent(TriggerEnterEvent event)
         -- Parameters
         local TriggerBodyEntity = event.TriggerBodyEntity
         --------------------------------------------------------
         local npcName = TriggerBodyEntity.NameTagComponent.Name
         local translateText = _LocalizationService:GetTextFormat("UI_TEXT_HELLO", npcName)
         _EntityService:GetEntityByPath("/ui/DefaultGroup/TextBox").TextComponent.Text = translateText
         end
         end
         



    
    
          
    
    9.  **[시작]** 버튼을 누른 뒤 테스트해 봅시다. 
        클라이언트의 언어 설정에 따라 적절한 메시지가 나옵니다.
        [gif](16741095688573170d1d86f0543609e51f38f2bce3942)
        [gif](1674109590083201ff51e11794e63b0343f945ff0e693)
    
    ## SmartFormat
    `GetText()`나 `GetTextFormat()`은 번역 테이블을 필요로 합니다. 번역 테이블을 사용하지 않고 `GetTextFormat()`의 포맷 기능만 따로 이용하고 싶다면 `SmartFormat()` 함수를 사용합니다.
    `SmartFormat()`에서는 인수로 **LocalizationTable**의 **Key** 대신 출력할 문자열을 직접 받습니다.
      
    
    [png](16750583548154a12eeaac0d048d39e1820ca865996a3)
      
    
    `SmartFormat()`를 사용해 위의 예제를 구현하면 아래와 같습니다.
    

@Component script TriggerGreetingDisplay extends Component @ExecSpace("ClientOnly") @EventSender("Self") handler HandleTriggerEnterEvent(TriggerEnterEvent event) \-- Parameters local TriggerBodyEntity = event.TriggerBodyEntity \-------------------------------------------------------- local npcName = TriggerBodyEntity.NameTagComponent.Name local translateText = _LocalizationService:SmartFormat("안녕, {0}!", npcName) _EntityService:GetEntityByPath("/ui/DefaultGroup/TextBox").TextComponent.Text = translateText end end
    
    
    하지만 이렇게 설정하면 클라이언트 언어 설정이 영어(en)일 때도 한국어로 "안녕"이 출력될 것입니다. 각 언어 설정에 맞게 출력하려면 이벤트 핸들러를 아래와 같이 수정합니다.
    

@Component script NpcGreetingDisplay extends Component @ExecSpace("ClientOnly") @EventSender("Self") handler HandleTriggerEnterEvent(TriggerEnterEvent event) \-- Parameters local TriggerBodyEntity = event.TriggerBodyEntity \-------------------------------------------------------- local npcName = TriggerBodyEntity.NameTagComponent.Name local localeId = _LocalizationService.CurrentLocaleId
    
    
       local translateTextKo = _LocalizationService:SmartFormat("안녕, {0}!", npcName)
       local translateTextEn = _LocalizationService:SmartFormat("Hello, {0}!", npcName)
    
       if localeId == "ko" then 
    _EntityService:GetEntityByPath("/ui/DefaultGroup/TextBox").TextComponent.Text = translateTextKo
       elseif localeId == "en" then
       	_EntityService:GetEntityByPath("/ui/DefaultGroup/TextBox").TextComponent.Text = translateTextEn
       end
    end
    

end
    
    
    # 포맷 활용하기
    
    위에서 살펴본 것처럼 **LocalizationService**에서는 텍스트의 변환 서식을 설정할 수 있는 두 가지 API를 제공합니다.
    
    * **GetTextFormat(string key, any... args)**
    * **SmartFormat(string format, any... args)**
    
    포맷에 포함되어 있는 **Argument hole**에는 단순한 단어가 들어갈 수도 있지만 특정 규칙에 맞춰 변환된 문자가 들어갈 수도 있습니다. 예를 들면 한국어 조사나 단수 복수 처리가 이에 해당합니다.
    **Argument hole**의 형식은 다음와 같습니다.
    [png](167411584046890fd957c327e4425ae0066bd4307301b)
    여기서 대괄호([])안의 내용은 선택적 요소들입니다. 즉, {0} 처럼 단순히 인덱스만 넣을 수도 있지만 **formatterName**과 **formatterArgument**를 설정할 수도 있습니다. **formatter**를 활용하면 각 언어 문화권의 문법에 맞고 자연스러운 문장으로 변환할 수 있습니다.
      
    
    **formatter**로 아래의 두 가지를 사용할 수 있습니다.
    | formatterName | 기능 | 설명 |
    | :---: | --- | --- |
    | hpp | 한국어 조사 처리 | 인수로 받은 문자열의 마지막 글자가 종성을 포함하는지 아닌지 여부에 따라 두 개의 formatterArgument중 하나를 출력합니다. |
    | plural 또는 p | 단수, 복수 처리 | 
    
     
        * 인수로 받은 숫자(숫자로만 구성된 문자열 포함)를 기반으로 명사 또는 단위 표현 시 복수형을 처리합니다.
     
        * formatterArgument의 개수는 언어마다 다릅니다.
     
        * 자세한 규칙은 를 참고합니다. |
        
        각 **formatter**을 활용하는 방법을 자세히 살펴봅시다.
        
        ## hpp : 한국어 조사 처리
        한국어에서 조사는 체언(명사, 대명사, 수사) 뒤에 붙어 문법의 의미를 더하는 품사입니다. 체언의 받침 유무에 따라 뒤에 붙는 조사가 달라집니다.
        예를 들어, **"나는 [   ]을/를 만납니다."** 라는 문장이 있다면 빈칸 [  ]에 들어갈 단어의 마지막 글자 종성 유무에 따라 조사 **'을'** 또는 **'를'**이 연결됩니다.
        
        | 빈칸에 들어갈 단어 | 조사 | 완성된 문장 |
        | :---: | :---: | :---: |
        | 스탄 | 을 | 나는 스탄을 만납니다. |
        | 리나 | 를 | 나는 리나를 만납니다. |
          
        
        이러한 처리를 할 수 있도록 해주는 것이 **hpp**입니다. 기본 형태는 아래와 같습니다.
        [png](1674181700829ead15381640f44de9a63d997522d22d0)
          
        
        | 기준 | 조사 |
        | --- | --- |
        | 앞의 체언에 종성 있을 때 | 은, 이, 을, 과, 아, 이여, 이랑, 으로 |
        | 앞의 체언에 종성 없을 때 | 는, 가, 를, 와, 야, 여, 랑, 로 |
          
        
        예제를 통해 hpp의 사용 방법을 알아봅시다.
        
        1. **LocalizationTable**을 열고 아래와 같이 3행을 수정합니다.
            | ID | Key | Source | ko | en |
            | :---: | --- | --- | --- | --- | 
            | 3 | UI_TEXT_HELLO | I meet {0}. | 나는 {0}{0:hpp:을\|를} 만납니다. | I meet {0}. |
              
        
        2. 이벤트 핸들러를 아래와 같이 수정합니다.
            ```lua
        @Component
        script TriggerGreetingDisplay extends Component
            @ExecSpace("ClientOnly")
            @EventSender("Self")
            handler HandleTriggerEnterEvent(TriggerEnterEvent event)
            -- Parameters
            local TriggerBodyEntity = event.TriggerBodyEntity
            --------------------------------------------------------
            local npcName = TriggerBodyEntity.NameTagComponent.Name
            local translateText = _LocalizationService:GetTextFormat("UI_TEXT_HELLO",npcName)
            _EntityService:GetEntityByPath("/ui/DefaultGroup/TextBox").TextComponent.Text = translateText
            end
        end
        
    
    
    
    
      
    
    

  3.  [시작] 버튼을 누른 뒤 테스트해 봅시다. 클라이언트의 언어 설정에 따라 적절한 메시지가 나옵니다. [gif](16741799873045e413c513a6a4640a91b930f3b1da366) [gif](16741799991931b62b30881134d439b8416dd3296fb7e)   




## plural : 단수, 복수 처리

한국어는 단수, 복수 관련 처리가 별도로 필요하지 않지만 단수, 복수 관련 처리가 필요한 언어도 있습니다. 이를 처리하기 위한 plural의 기본 형태는 아래와 같습니다. [png](167418048551082631b1ae8ec4e51b5e53e487bac5857)

예를 들어 영어는 1개일 때는 단수로 취급하지만 나머지는 복수로 취급합니다. 따라서 총 두 개의 formatterArgument 받습니다. [png](1674180498732cb6f503c59a940ab8340f688dd1470b6)

아래와 같이 활용해봅시다.
    
    
    @Component
    script LocalizationFormatter extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            _LocalizationService:SmartFormat("I have {0:plural:an apple|{} apples}.", 2) -- I have 2 apples.
        end
    end
    

위와 같이 formatterArgument 중 하나에만 인수로 받은 문자열을 출력하고 싶다면 비어있는 중괄호 {}를 사용합니다.

> Tip. plural은 p로 줄여서 쓸 수 있습니다.

## 예외

다음과 같이 포맷이 올바르지 않을 때는 원문 그대로 출력합니다.

##### formatterName이 틀린 경우

formatterName에 띄어쓰기가 포함되었거나, 오탈자가 있다면 원문이 그대로 출력됩니다.
    
    
    @Component
    script SmartFormatDemo extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            -- 1) 언어 설정 영어 : formatter name에 띄어쓰기가 포함된 경우 
            log(_LocalizationService:SmartFormat("I have {0: plural:an apple|{} apples}.", 2))
        end
    end
    

[png](1675214317926babf748aa5a14e4db96a7f039a29cc3d)   

    
    
    @Component
    script SmartFormatterDemo extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            -- 2) formatter name 자체가 틀린 경우 (예: hpp를 hp로 잘못 작성)
            log(_LocalizationService:SmartFormat("나는 {0}{0:hp:을|를} 만났다.", "스탄"))
        end
    end
    

[png](16752143336956dc58a0870ac444e9df8d70251208021)   
아래와 같이 수정하면 포맷에 맞게 제대로 변환된 문자열이 출력됩니다.
    
    
    @Component
    script PluralizationDemo extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            log(_LocalizationService:SmartFormat("I have {0:plural:an apple|{} apples}.", 2))
        end
    end
    

[png](1675214386354a550210300ea457d82d969f5ecb641a8)   

    
    
    @Component
    script EncounterLog extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            log(_LocalizationService:SmartFormat("나는 {0}{0:hpp:을|를} 만났다.", "스탄"))
        end
    end
    

[png](16752144025748615eeea77e545de9bb158996665c1ad)

  
##### 입력한 formatterArgument의 수가 요구하는 수와 다른 경우 이런 경우에는 상황에 따라 정상 출력될 수도 있고 포맷에 맞게 변환하는데 실패해 원문 그대로 출력될 수도 있습니다. 아래의 예시를 참고 바랍니다. ```lua @Component script SmartFormatPluralTester extends Component @ExecSpace("ServerOnly") method void OnBeginPlay() \-- 언어 설정 한국어 : plural 사용 log(_LocalizationService:SmartFormat("아이템 {0:plural:한 |여러 }개를 획득했다.", 1)) \-- 결과 : 원문 출력 (한국어 plural은 1개의 formatter argument만 받기 때문) end end ``` [png](1675213948789797bb2bfe84142ce88c090892532edf1) 
    
    
    @Component
    script LocalizationPluralDemo extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            -- 언어 설정 영어 : formatter argument의 개수가 필요한 개수와 다른 경우
            log(_LocalizationService:SmartFormat("I have {0:plural:one}.", 1))
            -- 결과 : 원문 출력 (영어 plural은 2개의 formatter argument를 받기 때문)
            
            -- 언어 설정 영어 : formatter argument를 공백으로 둘 때
            log(_LocalizationService:SmartFormat("I have {0:plural:|}.", 1))
            -- 결과 : 정상 출력 (빈 문자열을 formatter argument로 사용함)
        end
    end
    

[png](167521385644376cb49230ea64165b8e61d50f95b8c01)

  
##### 중괄호 {}의 쌍이 맞지 않는 경우 중괄호가 중첩되거나 포맷이 잘못된 경우에는 제대로 변환이 이루어지지 않으므로 원문 그대로 출력됩니다.   
##### formatArgument의 개수와 argument hole의 인덱스가 맞지 않는 경우 이런 경우에는 상황에 따라 정상 출력될 수도 있고 포맷에 맞게 변환하는데 실패해 원문 그대로 출력될 수도 있습니다. 아래의 예시를 참고 바랍니다. ```lua @Component script SmartFormatDemonstrator extends Component @ExecSpace("ServerOnly") method void OnBeginPlay() \-- 언어 설정 영어 : format argument의 개수가 argument hole의 최대 인덱스 +1보다 많음 log(_LocalizationService:SmartFormat("I have {0:plural:an apple|{} apples} and {1:plural:a cherry|{} cherries}.", 2, 1, 1)) \-- 결과 : 정상 출력 
    
    
        
        -- 언어 설정 영어 : format argument의 개수가 argument hole의 최대 인덱스보다 적음
        log(_LocalizationService:SmartFormat("I have {0:plural:an apple|{} apples} and {1:plural:a cherry|{} cherries}.", 2))
        -- 결과 : 원문 출력
        
        -- 언어 설정 영어 : format argument의 인덱스를 건너 뛰어서 사용
        log(_LocalizationService:SmartFormat("I have {1:plural:an apple|{} apples}.", 0, 1))
        -- 결과 : 정상 출력
    end
    

end
    
    
    [png](1675214079174ce82f7da440c49c1a1208d193461cad1)