대사와 캐스트
콘텐츠는 씬의 대사와 내레이션 텍스트입니다. 모든 콘텐츠 줄은 같은 형태를 가집니다.
@speaker{attributes}: the text they say화자는 줄의 종류를 선택합니다.
- 예약된
narrator→ 내레이션(화자 없음) - 그 밖의 화자 id → 해당 화자를 담는 대사
프런트매터의 pov는 씬의 시점 캐릭터를 지정하며, 없으면 프로젝트의 defaults.pov를 사용합니다.
이는 해당 화자의 일반 줄이 컴파일되는 방식을 바꾸지 않습니다. 주인공의 줄도 대사이고, 아티팩트에는 pov나
플레이어 역할이 들어 있지 않습니다. pov는 {mono}를 말할 수 있는 사람을 결정합니다.
주인공을 다르게 렌더링하는 엔진(초상화 없음, 플레이어가 선택한 이름을 라벨로 표시)은 화자 id로 이를 결정합니다.
줄 텍스트에서는 플레이어 이름에 {{userName}}을 씁니다.
별도의 생각 노드나 산문 노드는 없습니다 — 역할은 화자와 그 전달 방식(아래 참조)에서 파생됩니다.
@narrator: Venny's again. The chain restaurant that has never offended anyone.@marina{code="0010" emotion="delighted" variant="1"}: Mr. Fixer! You came back!@fixer{code="0010"}: I did.(docs/examples/showcase/episode01.lute에서 가져왔습니다.)
콘텐츠는 섹션에 들어갑니다. 섹션은 ## 헤딩에서 시작해 다음 헤딩까지 이어지며, 헤딩 텍스트는 작가와
도구를 위한 것입니다. 문서에는 본문 제목이 없습니다 — 제목은 프런트매터의 title:이고, 본문에 # 헤딩을
쓰면 E-INERT-TITLE입니다.
헤딩은 끝에 안정적인 id {#id}를 붙일 수 있습니다. id는 문자로 시작하고 뒤에 최대 63개의 문자, 숫자,
-, _가 올 수 있으며 문서의 섹션 사이에서 고유해야 합니다(그렇지 않으면 E-SECTION-DUP). id는 헤딩
텍스트의 일부가 아닙니다.
---kind: sceneid: diner.nighttitle: Night Shift---
## The Counter {#counter}
@narrator: The diner hums.
## Closing Time
@narrator: The lights go out one by one.아티팩트는 문서 순서대로 섹션을 나열하며, 1부터 번호를 매깁니다.
"sections": [ {"section": 1, "heading": "The Counter", "id": "counter"}, {"section": 2, "heading": "Closing Time"}]섹션의 번호는 그 안의 모든 record position의 첫 세그먼트이기도 합니다(001-0100은 섹션 1의 첫 record).
position과 번호는 소스 순서를 따르므로 섹션이 이동하면 바뀝니다. 편집을 거쳐도 섹션의 정체성을 유지해야 하는
도구는 {#id}를 읽으며, id가 없는 섹션은 안정적인 정체성이 없는 것으로 보고합니다. 섹션 id는 정체성일
뿐입니다. 점프 대상이 아니며 — ::jump는 ::label로 이동합니다.
줄 속성
섹션 제목: “줄 속성”{…} 안의 속성은 콘텐츠 메타데이터입니다. code(줄마다 안정적인 id), emotion, variant, action,
dialogMotion, as(한 줄에만 적용되는 화자 라벨 오버라이드)가 있습니다. 속성의 도메인은 문법이 아니라
프로젝트 어휘입니다 — lute context <file>로 프로젝트에서 허용하는 emotion/variant 값을 나열할 수 있습니다.
어느 속성도 필수는 아닙니다. code가 없으면 컴파일 시 결정론적으로 나중에 채워지며 lute tag로 저장할 수 있습니다.
모든 줄은 lineId와 voiceKey로 컴파일됩니다 — 내레이션과 {mono} 줄도 포함됩니다.
{"kind": "line", "family": "content", "position": "001-0100", "role": "narration", "speaker": "narrator", "text": "The diner hums.", "lineId": "diner.night.narrator_0010", "voiceKey": "diner.night.narrator-0010"}voice key는 녹음을 위한 조인 키이지 녹음이 존재한다는 주장이 아닙니다. 내레이션에 음성을 입히지 않는 프로젝트는
그 키에 해당하는 아티팩트가 없을 뿐입니다. 프로젝트에서 같은 voiceKey를 공유하는 두 줄은 동일한 텍스트를
말해야 합니다(인라인 수식자를 제거한 뒤 비교). 그렇지 않으면 프로젝트 전체 lute check가
E-DUP-VOICEKEY를 보고합니다.
action=은 무대 결과가 있는 유일한 줄 속성이며, 그것도 작습니다. 해당 줄에서 화자의 포즈를 설정하고
화자를 변경된 상태(dirty)로 표시하므로, 같은 화자의 다음 일반 줄 앞에 posReset이 삽입됩니다
(provenance.by: "auto-pose-reset"). 이것은 무대 규칙이 아니라 전달 세부사항입니다.
캐릭터의 등장과 퇴장은 actor로 낮춰진 모든 스테이징 레코드(::actor 포함)가 담당하며,
exit: true 필드가 해당 캐릭터의 presence를 끝냅니다. ::clear(0.24.0)는 모두의 presence를
끝냅니다(코어 디렉티브 참고). lute context가 코어 디렉티브의
actor 의미론에 mayExitCharacter를 나열하는 이유도 이것입니다. 콘텐츠 줄에 action 도메인의
exits: 목록 멤버를 쓰면 W-EXIT-INERT입니다. 포즈는 적용되지만 캐릭터는 무대에 남고 아티팩트에는
exit record가 생기지 않습니다. 선언된 actor 퇴장, (플러그인 디렉티브를 포함해) bg로 낮춰진
스테이징 레코드, 또는 ::clear로 캐릭터가 떠난 뒤 actor 레코드로 다시 무대에 올리기 전 그
캐릭터의 줄은 W-STAGE-ABSENT입니다. 선택지와 <match> 갈래를 통하는 모든 경로에서 판정됩니다
(무대 상태 참고).
@marina{as="???"}: ...who's there?as는 해당 줄의 표시 라벨만 덮어씁니다. 없으면 줄은 화자 id를 라벨로 렌더링하고, 엔진은 그 id를 프로젝트
캐스트가 제공하는 표시 이름인 name에 매핑합니다. 소스에서 라벨을 설정하는 유일한 방법은 as=입니다.
더 풍부한 표시 이름 기능 — 의상과 이름 공개를 포함하는
character/cast 제안 — 은 초안이며,
그런 플러그인은 아직 제공되지 않습니다.
캐스트
섹션 제목: “캐스트”프로젝트는 캐스트(dsl 0.23.0)를 선언할 수 있습니다. 캐스트는 줄에서 사용할 수 있는 화자 id의 집합이며,
각 id에 선택적인 표시 이름을 둘 수 있습니다. 스키마 문서는 cast: 아래에 이를 선언하고, 스키마를 가져오는
문서가 그에 맞는지 검사됩니다.
cast: mira: { name: Mira } oskar: { name: "Oskar Lind" } vesna: {}플러그인도 같은 형태의 cast/*.yaml 파일을 cast export로 제공할 수 있습니다(매니페스트 참고).
그러면 엔진 팩이 초상화와 음성이 존재하는 캐릭터를 선언할 수 있습니다.
캐스트가 선언되면 모든 화자는 캐스트 안에 있어야 합니다. 씬 줄, 퀘스트 본문, 로어 엔트리,
번들 비트를 모두 검사합니다. 캐스트 밖의 화자는 가까운 오타에 대한
did-you-mean 제안(예: @oskr)과 함께 E-CAST-UNKNOWN이 됩니다. narrator는 항상 화자입니다. pov 화자는
특별하지 않습니다. pov가 fixer인 씬도 fixer가 캐스트에 있어야 합니다.
./scenes/arrival.lute:12:2: error [E-CAST-UNKNOWN] speaker `fixer` is not in the declared cast캐스트를 선언하지 않으면 0.23.0 이전과 마찬가지로 화자는 형태만 검사되므로 프로젝트는 캐릭터를 확정하기 전에도
대사를 작성할 수 있습니다. 캐스트는 씬 하나가 아니라 프로젝트에 속합니다. 씬 프런트매터의 cast: 키는
E-META-UNKNOWN-KEY입니다.
lute context <file>는 표시 이름과 함께 캐스트를 나열합니다(--project <DIR>를 전달하면 플러그인의 캐스트도
포함). 언어 서버는 @ 뒤에 화자를 완성할 때 캐스트를 제안합니다.
두 화자가 공유하는 표시 이름
섹션 제목: “두 화자가 공유하는 표시 이름”대화 상자에서 같은 이름으로 표시되는 두 화자는 한 사람처럼 보입니다. 여러 작가가 하나의 프로젝트에 캐릭터를
추가할 때 두 지역에서 각각 Hiker Gus를 작성하는 식으로 우연히 발생합니다. 프로젝트 전체 lute check는 이를 권고
W-DISPLAY-NAME-DUP(dsl 0.26.0 §2.8)으로 보고하며, lute lint도 보고합니다. 다음 두 경우처럼
정확히 같은 name:을 가진 캐스트 항목, 컴포넌트의 ::use{… name="…"} 표시 문자열과 같은 캐스트 이름,
서로 다른 화자에 대한 두 who= 문자열을 비교합니다.
./scenes/a.lute:11:1: warning [W-DISPLAY-NAME-DUP] display name `Hiker Gus` is shown for 2 different speakers: `gus` (./scenes/a.lute:10), `gus2` (./scenes/a.lute:11) — the dialogue box cannot tell them apart; rename one일부 이름은 의도적으로 공유합니다. 예를 들어 악당 팀의 계급처럼 여러 화자가 맡는 역할입니다. 그런 항목에
sharedName: true를 표시하면 집계되지 않습니다.
cast: gus: { name: Hiker Gus } gus2: { name: Hiker Gus Jr. } grunt1: { name: Eclipse Grunt, sharedName: true } grunt2: { name: Eclipse Grunt, sharedName: true }플러그인의 cast/*.yaml 항목도 같은 키를 받습니다. lute check --project . --deny-warnings를 실행하는 병합 게이트는
의도한 역할 이름에 표시를 붙이면 다시 통과하고, lute lint --deny W-DISPLAY-NAME-DUP는 이를 자체적으로 코드로
승격합니다. 여러 작가 프로젝트를 참고하세요.
연출도 캐스트에 맞춰 검사됩니다
섹션 제목: “연출도 캐스트에 맞춰 검사됩니다”캐스트가 선언되면 연출 디렉티브가 지정하는 캐릭터도 화자와 마찬가지로 캐스트 안에 있어야 합니다(dsl 0.24.0 §4).
캐스트 밖의 ::actor{character}와 ::camera{focus}는 동일한 did-you-mean과 함께 E-CAST-UNKNOWN입니다.
타임라인 클립, <match> 갈래와 선택지의 본문도 검사됩니다.
./scenes/ridge.lute:11:20: error [E-CAST-UNKNOWN] `::actor{character}` `corvn` is not in the declared cast — did you mean `corvin`?./scenes/ridge.lute:12:17: error [E-CAST-UNKNOWN] `::camera{focus}` `isold` is not in the declared cast — did you mean `isolde`?화자별 감정
섹션 제목: “화자별 감정”캐스트 항목에는 캐릭터가 표현하는 감정 emotions: [calm, fierce]을 나열할 수 있습니다(dsl 0.24.0 §4).
각 멤버는 프로젝트의 emotion enum 멤버이기도 해야 합니다. 해당 화자의 줄에서, 또는 리터럴 character=가
그 화자를 지정한 디렉티브에서 목록 밖의 emotion=을 쓰면 emotion enum에 그 값이 있더라도 E-BAD-ENUM입니다.
./scenes/ridge.lute:14:18: error [E-BAD-ENUM] `sad` is not one of `isolde`'s emotions (expected one of: calm, fierce) — the cast declares `emotions:` for `isolde`emotion enum 자체가 거부하는 값은 기존의 오류 하나를 그대로 냅니다. emotions:가 없는 화자는 enum의 어떤
멤버든 사용할 수 있습니다.
Presence: present:
섹션 제목: “Presence: present:”파티 게임에서 동료가 항상 곁에 있어 말할 수 있는 것은 아닙니다. 캐스트 항목은 조건인
present: "<condition>"으로 캐릭터의 존재 시점을 지정할 수 있습니다(dsl 0.24.0 §4). 조건은 일반 상태나
팩트이므로 파티 멤버십에 새 메커니즘이 필요하지 않습니다.
entities: person: { members: [isolde, corvin] }relations: inParty: { args: [person], tier: run }cast: isolde: { name: Isolde, present: "holds('inParty', ['isolde'])", emotions: [calm, fierce] } corvin: { name: "Corvin Hale" }플러그인의 cast/*.yaml 항목도 같은 두 키를 받습니다(매니페스트 참고).
그러면 검사기는 isolde가 말하는 모든 줄에 대해 줄을 둘러싼 가드가 그녀의 present: 조건을 함의하는지 묻습니다.
그렇지 않으면 줄은 W-CAST-ABSENT입니다.
@corvin: Where is Isolde?@isolde: Right here.@isolde{when="holds('inParty', ['isolde'])"}: Right behind you.<branch id="ask"> <choice id="call" text="Call her over" when="holds('inParty', ['isolde'])"> @isolde{emotion="calm"}: I came. ::retract{ inParty(isolde) } @isolde: And now I'm leaving. </choice> <choice id="wait" text="Wait"> @corvin: We wait, then. </choice></branch>::assert{ inParty(isolde) }@isolde: Back again.@isolde: Right here.에는 가드가 없으므로 경고합니다. 다음 줄과 I came.은 조건 자체로 가드됩니다.
And now I'm leaving.은 선택지의 가드와 줄 사이에서 ::retract가 일어나 가드가 말하던 조건을 거짓으로
만들기 때문에 다시 경고합니다. 메시지는 조건과 이를 만족할 가드를 이름으로 표시합니다.
./scenes/camp.lute:12:2: warning [W-CAST-ABSENT] `isolde` may not be here: the cast declares `present: "holds('inParty', ['isolde'])"` for `isolde`, and the guards around this line do not imply it. Guard the line — `@isolde{when="holds('inParty', ['isolde'])"}` — or move it under a guard that implies it유효한 가드는 다음과 같습니다. 줄 자체의 when=, (branch 또는 hub 안에서) 감싸는 모든 <choice when>,
<match> 갈래(갈래의 is= 또는 test, 그리고 앞선 갈래가 일치하지 않았다는 사실), <on when>,
<objective done>, 씬 비트의 when:, 엔트리의 when= 또는 번들 비트의 when=입니다. 그 안의 @def는
확장됩니다. 줄이 놓인 위치만으로 암시되는 가드도 있습니다.
- 로어 엔트리 본문은 자신의
entry.<id>.read/everRead를 전제로 합니다. - 퀘스트
<on>핸들러는 해당 이벤트에서 퀘스트의 상태를 전제로 하며, 한 번 참이 되면 유지되는start의 결합항 (entry.<id>.everRead,visited(…))도 전제로 합니다. - 퀘스트의
questComplete핸들러도 퀘스트 완료를 전제로 합니다(dsl 0.27.0). 퀘스트는 필수 objective 중 하나가 완료되면 완료되므로, 필수 objective 하나의done이 참입니다. 필요한 objective 모두가@withToby를 요구하는 퀘스트의 핸들러라면 Tobias에게 줄을 주면서 가드를 반복하지 않아도 됩니다. objective 본문이나 이 퀘스트의 이전questComplete핸들러에 있는::assert,::retract,::set은 다른 가드와 마찬가지로 이 전제를 취소할 수 있습니다. - 프로젝트 전체
lute check에서는 같은 계기에 대해 항상 자격이 있는once비트 중 자신보다 순위가 높은 비트가 이미 소진되었다고 전제합니다.
파생 관계에 대한 holds(A) 가드는 A의 규칙 본문도 함의합니다. inParty(P) :- recruited(P), not departed(P)라면
holds('inParty', ['isolde'])로 가드된 줄은 present: "!holds('departed', ['isolde'])"를 만족합니다. 한 번
바인딩되면 ground로 고정되는 관계의 규칙(예: cel()만 사용하는 일정)은 규칙 본문의 논리합으로 읽힙니다.
쓰기가 가드를 취소하는 것은 그 경로에서 가드를 거짓으로 만들 수 있을 때뿐입니다. ::assert나 ::retract는
같은 관계의 통합 가능한 인수를 가진 가드 atom을 무효화하거나, 작성한 관계를 전제로 하는 규칙을 통해 유도된
atom을 무효화합니다. 그것도 atom을 잘못된 방향으로 이동할 때만 그렇습니다. inParty(corvin)을 assert해도
holds('inParty', ['isolde']) 가드는 유지되며, 그 가드의 양의 전제를 assert해도 유지됩니다. ::set은 해당 경로를
읽는 가드를, 그런 규칙을 통한 경우를 포함해 무효화합니다. 가드의 각 && 결합항은 자체적으로 판정되며, 한
<choice>나 <match> 갈래의 쓰기는 형제 갈래에 도달하지 않습니다.
{vo} 줄은 예외입니다. voice-over는 화자가 그 자리에 있다는 뜻이 아니므로 기억이나 장면 위의 내레이션처럼
씬의 시간 밖에서 말할 수 있습니다. {os} 줄은 검사됩니다(dsl 0.25.0 D-E). {os}는 화자가 씬 안에 있지만
화면 밖에서 들린다는 뜻이므로 presence가 있어야 합니다. 라디오나 전화 너머의 원격 화자처럼 별도의 원격 화자
플래그는 아직 없습니다.
팩트는 lute check <FILE>와 프로젝트 전체 lute check가 다른 유일한 곳입니다. 프로젝트 전체 lute check는 줄까지 이어지는 모든 경로에서
참인 팩트도 셉니다. 모든 경로에서 앞서 assert되었거나, seed된 뒤 retract되지 않은 팩트입니다. 따라서 위 예시의
마지막 줄 ::assert{ inParty(isolde) } 뒤의 @isolde: Back again.은 그곳에서는 깨끗합니다. 단일 파일
lute check는 그런 팩트를 볼 수 없으므로 그 줄에도 경고하고, 프로젝트 전체 lute check라면 볼 수 있다는 참고 사항을 추가합니다.
다른 경고와 마찬가지로 --deny W-CAST-ABSENT는 이를 오류로 올립니다.
Presence에는 엔진만 쓰는 두 번째 조건이 붙는 경우가 많습니다. 예를 들어 “파티에 있고, 쓰러지지 않았다”인
present: "holds('inParty', ['isolde']) && !holds('fell', ['isolde'])"이며 fell은 reserved: true 관계입니다.
검사기는 엔진이 fell을 언제 쓰는지 모르므로, 도움 없이는 각 줄 또는 비트에 가드가 있어야 그 절반을 만족하고
isolde의 모든 줄에서 경고합니다. assume: true(dsl 0.24.0 §4)를 추가한 캐스트 항목은 present:의
reserved: 관계에 대한 모든 부정 holds를 참으로 읽습니다. 직접 부정했든
inParty(P) :- recruited(P), not fell(P) 같은 규칙을 통해 부정했든 동일합니다. 그러면 위 줄들은 일반
holds('inParty', ['isolde']) 조건만 있었을 때와 정확히 같이 경고합니다. 대가는 엔진 이벤트 뒤에 말하는 줄에는
자체 가드가 필요하다는 것입니다. 단, 관계가 변경되는 이벤트를 changedOn,
dsl 0.25.0으로 이름 붙이면 예외입니다.
cast: isolde: { name: Isolde, present: "holds('inParty', ['isolde']) && !holds('fell', ['isolde'])", assume: true }present:, emotions:, assume:은 검사기의 입력입니다. 어느 것도 컴파일된 아티팩트에 들어가지 않습니다.
엔진 이벤트 이후의 presence: changedOn
섹션 제목: “엔진 이벤트 이후의 presence: changedOn”assume: true는 예약된 관계가 의미하는 엔진 이벤트가 일어나기 전까지는 올바릅니다. 전투 후에는 fell(isolde)가 참일 수 있고, 바로 그곳에서 검사가 중요해집니다. 예약된 관계는 엔진이 어떤 계기에서 그 값을 바꾸는지 말할 수 있습니다(dsl 0.25.0 §6):
entities: companion: { members: [isolde] }relations: inParty: { args: [companion], tier: run } fell: { args: [companion], tier: run, reserved: true, changedOn: [battleEnd] }cast: isolde: { name: Isolde, present: "holds('inParty', ['isolde']) && !holds('fell', ['isolde'])", assume: true }changedOn이 있으면 assume: true는 해당 계기에서 제시되는 단위에서 더 이상 !holds('fell', ['…'])를 참으로 읽지 않습니다. 여기에는 on: battleEnd에 응답하는 씬, 엔트리 또는 번들 비트, 그리고 그곳에서 판정되는 퀘스트 <on event> 핸들러나 on= 목표 본문이 포함됩니다. 프로젝트 전체 lute check에서는 시나리오 그래프에서 그러한 단위의 이후 자손(after-descendant) 모두에 대해서도 마찬가지이며, after: / after= / [start] 간선이 적용됩니다. 계기 자체에는 정적인 순서가 없으므로, 무엇이 “전투 이후”인지 말해 주는 것은 그래프입니다. inParty(isolde)를 단언하는 캠프 씬, after: 'visited("camp")'인 battleEnd의 여파 씬, 그리고 after: 'visited("aftermath")'인 길 씬을 생각해 보세요:
@isolde{when="holds('inParty', ['isolde'])"}: We should keep moving.@isolde{os}: Wait for me!@isolde{vo}: I remember that road.길에서는 첫 두 줄이 다시 경고되며, 각각 assume: true가 적용되지 않은 이유를 말하는 참고 사항이 붙습니다. {vo} 줄은 계속 예외입니다:
./scenes/road.lute:11:2: warning [W-CAST-ABSENT] `isolde` may not be here: the cast declares `present: "holds('inParty', ['isolde']) && !holds('fell', ['isolde'])"` for `isolde`, and the guards around this line do not imply it. Guard the line — `@isolde{when="holds('inParty', ['isolde']) && !holds('fell', ['isolde'])"}` — or move it under a guard that implies it; `assume: true` does not cover `fell`: this line follows an occasion its `changedOn:` names그 줄들에 전체 조건인 @isolde{when="holds('inParty', ['isolde']) && !holds('fell', ['isolde'])"}으로 가드를 붙이면 경고가 사라집니다. 그래프에서 전투보다 앞에 있는 캠프 씬의 줄은 assume: true의 적용을 계속 받습니다. changedOn이 없으면 0.24 동작은 변하지 않으며, assume: true가 모든 줄에 적용됩니다. reserved: true가 아닌 관계에 changedOn을 쓰면 E-RELATION-DECL이고, 플러그인이 선언하지 않은 계기도 did-you-mean과 함께 같은 오류가 됩니다(`changedOn: batleEnd` is not a declared occasion — did you mean `battleEnd`?). 프로젝트의 플러그인이 계기를 하나도 선언하지 않으면 이름을 검사하지 않습니다.
무대에서 내보내기: ::clear
섹션 제목: “무대에서 내보내기: ::clear”presence는 플레이어와 함께 있는 사람이 누구인지에 대한 것입니다. 무대에 올라와 있는 사람이 누구인지는 무대 상태입니다(무대 상태 참고). ::actor는 캐릭터를 무대에 올리고, 선언된 퇴장이나 ::bg가 캐릭터를 무대에서 내립니다. 0.24.0부터 말단 ::clear는 배경과 음악은 그대로 둔 채 모두를 한 번에 무대에서 내립니다(핵심 디렉티브 참고). ::actor로 다시 보여 주기 전에, 무대에서 지워진 캐릭터가 말하는 줄에는 W-STAGE-ABSENT가 표시되고 경고는 ::clear를 지목합니다.
전달 플래그
섹션 제목: “전달 플래그”전달 플래그(delivery flag)는 중괄호 안의 맨 단어(=value 없음)로, 줄이 전달되는 방식을 바꿉니다:
{mono}— 속마음/생각(씬 안에서 소리 내어 말하지 않음).{os}— 화면 밖(off-screen): 화자의 목소리는 들리지만 현재 무대에 올라와 있거나 보이는 상태는 아님.{vo}— 보이스오버(voice-over): 씬 위에 겹쳐지는 내레이션 방식의 전달.
@fixer{mono}: An android, then. Which would, on reflection, explain the ramen.세 플래그는 상호 배타적이며 한 줄에 최대 하나만 쓸 수 있습니다(둘을 함께 쓰면 E-DELIVERY-CONFLICT). 또한 @narrator에는 어느 것도 허용되지 않습니다(E-DELIVERY-NARRATOR).
역할은 화자와 전달 방식에서 파생되며, 아티팩트의 role도 같은 단어를 사용합니다: narrator → narration, {mono} → mono, {os} → os, {vo} → vo, 그 밖의 모든 캐릭터 → dialogue입니다.
{mono}로 말할 수 있는 사람
섹션 제목: “{mono}로 말할 수 있는 사람”{mono} 줄은 시점 인물의 속마음입니다. 유효 POV는 문서의 pov:이고, 없으면 프로젝트의 defaults.pov입니다. 화자가 유효 POV이거나 monoSpeakers:에 나열되어 있을 때 {mono} 줄이 유효합니다. monoSpeakers:는 문서 프런트매터의 화자 id 목록이며, 프로젝트의 defaults:에 두어 모든 문서에 적용할 수도 있습니다(문서 자체의 목록이 우선합니다). 두 번째 주인공처럼 내면의 목소리를 공유하는 씬에 사용하세요:
---kind: sceneid: diner.nightpov: fixermonoSpeakers: [mira]---
## The Counter
@fixer{mono}: Another night, another coffee.@mira{mono}: He looks tired.@mira{os}: Order up!@fixer{vo}: I remember that night.monoSpeakers: [mira]가 없으면 @mira{mono}는 E-MONO-POV입니다. POV가 전혀 해석되지 않을 때 — pov:도 defaults.pov도 없을 때 — monoSpeakers 밖의 화자가 쓴 {mono} 줄은 E-MONO-NO-POV입니다:
diner.lute:10:7: error [E-MONO-POV] `@mira{mono}` is an interior monologue, but `mira` is not this document's point of view (`fixer`) and not in `monoSpeakers:` (which lists none) — only the POV character and the speakers `monoSpeakers:` lists may speak `mono`; add `mira` to `monoSpeakers:`, or write the line as dialogue, `{os}` or `{vo}`컴포넌트의 {mono} 줄은 각 ::use에서 호출하는 문서의 POV와 monoSpeakers를 기준으로 검사됩니다. 오류는 ::use 위치에 보고되며 컴포넌트의 해당 줄을 지목합니다.
콘텐츠 Text(와 아래의 라벨 문법 값)에는 렌더링 시 게임 상태를 읽는 {{…}} 보간을 넣을 수 있습니다:
@narrator: Good to see you, {{userName}}.@marina{code="0010" emotion="delighted" variant="1"}: You came back! Warmth so far: {{run.affection}}.(docs/examples/showcase/hub-demo.lute에서 가져왔습니다.)
{{userName}}은 항상 사용할 수 있는 예약 토큰입니다. 그 밖의 맨 이름은 플러그인이 선언하는 호스트 토큰이고, 그렇지 않은 보간은 선언된 상태 경로를 지정해야 합니다. 보간은 확정 할당 분석에서 읽기이므로, 가드 없이 설정되지 않을 수 있는 경로를 보간하면 E-MAYBE-UNSET입니다. 보간과 인라인 수식자를 제외하면 두 번째 콜론 뒤의 텍스트는 줄 끝까지 불투명합니다. 괄호, <, //, #, 단일 중괄호 등은 모두 리터럴이며 파싱되지 않습니다. 검사기는 Ink 또는 Yarn에서 마크업으로 쓰이는 모양을 플레이어가 보게 되는 경우 경고합니다. 단일 중괄호 {run.oil} 또는 {run.oil > 0: …}(W-TEXT-SINGLE-BRACE; 보간은 {{run.oil}}, 조건부 텍스트는 가드된 줄), // note나 끝의 # tag(W-TEXT-COMMENT-LIKE; 주석은 자체 줄에 //를 씁니다)가 그 예입니다. Ink 또는 Yarn에서 넘어오기를 참고하세요.
표시 labels:가 붙은 enum에 대해 타입이 지정된 경로는 멤버 id가 아니라 라벨을 렌더링합니다: Today is {{run.weekday}}.는 Today is Sunday.로 읽힙니다(이름 붙은 enum으로 타입 지정된 경로 참고).
라벨과 프롬프트
섹션 제목: “라벨과 프롬프트”따옴표 속성 값 가운데 세 가지가 라벨 문법을 따릅니다. <choice>의 text, <branch prompt>·<hub prompt>, 그리고 type: text로 선언된 플러그인 디렉티브 속성입니다. 이 값에는 형식 힌트를 포함한 {{…}} 보간을 쓸 수 있고 인라인 수식자는 쓸 수 없습니다. 0.52부터 프롬프트는 선택지 라벨과 똑같이 검사됩니다. 선언되지 않은 경로는 E-CEL-PROFILE, 설정되지 않았을 수 있는 경로는 E-MAYBE-UNSET이며, 런타임은 프롬프트의 마커를 라벨과 함께 렌더링해 메뉴에 싣습니다(선택지와 허브 참고). 세 값 모두에서 \{{는 줄 텍스트에서처럼 리터럴 {{를 씁니다(0.52, #211). text="Say \{{open}}"은 Say {{open}}으로 보이고 플레이스홀더가 없습니다. 라벨 문법을 따르지 않는 따옴표 값(::bg의 location, when, ::use 인자 등)에서 \{{는 E-STRING-ESCAPE입니다:
br.lute:17:16: error [E-STRING-ESCAPE] `\{{` (a literal `{{`) is defined only in line text and in the values that take the label grammar — `<choice text>`, `prompt` and a plugin `text` attribute; `location` of `::bg` is not one::use 인자는 리터럴 {{를 \\{{로 넘깁니다(컴포넌트). <objective title>은 라벨 문법이 아니라 평문입니다. 퀘스트 UI는 제목을 쓰인 그대로 보여주므로 그 안의 {{는 E-OBJECTIVE-TITLE-INTERP입니다(퀘스트와 씬).
호스트 토큰
섹션 제목: “호스트 토큰”줄은 플레이어 이름처럼 엔진만 아는 값을 맨 식별자로 가리킬 수 있습니다: {{playerName}}. 0.50부터 플러그인은 hostTokens export에서 이런 이름을 선언하며, lute.core는 userName: { type: string }을 선언합니다. hostTokens: { playerName: { type: string } }가 있으면
@narrator: Welcome, {{playerName}}.는 reserved 플레이스홀더 { "kind": "reserved", "token": "playerName", "format"? }로 컴파일됩니다. 토큰은 맨 식별자(ASCII 문자 또는 _, 이어서 문자, 숫자, _)입니다. {{player-name}}과 {{2nd}}는 토큰이 아닙니다. 상태 경로 모양으로 남으며 기존의 E-CEL-PROFILE을 그대로 받습니다. 상태 경로에는 항상 티어 루트와 .이 있으므로, 상태 경로이던 표기의 의미가 바뀌는 일은 없습니다. 상태 루트나 CEL 리터럴인 이름은 토큰으로 선언할 수 없습니다(예약어 참고).
- 선언된 토큰. 토큰의 타입이 받을 수 있는 형식 힌트를 정합니다.
string또는enum토큰은 텍스트 힌트를 받고({{platform:capitalize}}는 깨끗합니다) 숫자 힌트는E-REF-TYPE입니다.int토큰은 숫자 힌트를 받고({{rank:ordinal}}은 깨끗합니다) 텍스트 힌트({{rank:capitalize}})는E-REF-TYPE입니다.{{userName}}은 이전과 똑같이 동작합니다. - 선언되지 않은 토큰. 어떤 플러그인도 선언하지 않은 맨 이름은
E-CEL-PROFILE입니다. 메시지는 “a declared host token”을 언급하고 선언된 토큰을 나열합니다:interpolation `playerName` is not a valid `{{…}}` form — only a state path, …, a def `@ref` / `@ref(args)`, or a declared host token (`userName`) are permitted; declare `playerName` in a plugin's `hostTokens:` export if the engine substitutes it …. 메시지의 나머지는 그대로입니다. - 이스케이프.
\{{name}}은 그대로입니다. 플레이스홀더가 되지 않습니다. - 상태 읽기가 아님. 호스트 토큰은 확정 할당, 의존성, 퀘스트 참조 분석에 참여하지 않으며 lint에서는 한 단어로 셉니다.
- 런타임. 런타임은 모든 호스트 토큰 표시를 렌더된 텍스트에 그대로 남기고, 호스트나 프레젠터가 이를 치환합니다.
userName만이 아니라 모든 토큰에 해당합니다(런타임 계약 참고). 에디터 호버는host token `playerName` — `string`, declared by `acme.stage`; the engine substitutes it at render time라고 읽습니다.
서수: {{x:ordinal}}
섹션 제목: “서수: {{x:ordinal}}”숫자 값은 콜론 뒤의 형식 힌트(format hint)로 영어 서수로 렌더링할 수 있습니다(dsl 0.24.0 §4):
---kind: sceneid: tower.gatestate: user.climbs: { type: int, default: 1 }---
## The Gate
@narrator: Welcome back, {{userName}}. This is your {{user.climbs:ordinal}} climb.lute run, lute play, lute trace, lute test는 1st 2nd 3rd 4th … 11th 12th 13th … 21st 22nd … 101st 111th를 렌더링합니다. 서수가 없거나, 분수이거나, 음수인 값은 그대로 렌더링됩니다(2.5, -3). 아티팩트에서 줄의 플레이스홀더는 "format": "ordinal"을 가지며 엔진이 렌더링합니다. 힌트가 없는 플레이스홀더에는 format 키가 없으므로, 이를 사용하는 아티팩트는 변경되지 않습니다.
서수 단어: {{x:ordinalWord}}
섹션 제목: “서수 단어: {{x:ordinalWord}}”:ordinalWord(dsl 0.25.0 §8)는 서수를 단어로 렌더링하여 글자로 풀어 쓴 편이 자연스러운 텍스트에 사용합니다:
---kind: sceneid: road.campstate: run.day: { type: int, default: 1 }---
## Camp
@narrator: The {{run.day:ordinalWord}} night on the road.IR 플레이스홀더는 "format": "ordinalWord"를 가지며 엔진이 단어를 지역화합니다. lute run, lute play, lute trace, lute test는 1–20에 대해 영어 단어 first second … twentieth를 렌더링하고, 그 범위를 벗어나면 :ordinal 숫자로 대체합니다(0th, 21st). 검사기는 :ordinal이 허용되는 모든 곳에서 :ordinalWord를 허용합니다.
수사 단어와 복수형: {{x:cardinalWord}}, {{x:plural(one|other)}}
섹션 제목: “수사 단어와 복수형: {{x:cardinalWord}}, {{x:plural(one|other)}}”:cardinalWord는 숫자를 one two … twenty로 풀어 쓰고 그 범위를 벗어나면 숫자로 대체합니다(0, 21). 검사기는 :ordinal이 허용되는 모든 곳에서 이를 허용합니다. :plural(singular|plural)은 숫자가 1일 때 첫 번째 형식을, 그 밖에는 두 번째 형식을 고릅니다. 형식 안에서 #는 숫자, #word는 숫자의 기수 단어, #Word는 문장 첫머리에 맞춘 대문자 기수 단어입니다:
---kind: sceneid: road.wagonsstate: run.wagons: { type: int, default: 11 }---
## Camp
@narrator: {{run.wagons:plural(One wagon waits|#Word wagons wait)}} at the ford, {{run.wagons:cardinalWord}} in all.이 줄은 Eleven wagons wait at the ford, eleven in all.로 읽힙니다. 복수형은 |로 구분하는 평문입니다. 따옴표로 감싼 형식(plural("One wagon"|"# wagons"))은 따옴표를 출력하고, , 구분자(plural(wagon,wagons))는 하나의 형식이므로 둘 다 E-PLURAL-FORM이며 메시지에 수정 방법이 표시됩니다.
텍스트 힌트와 라벨 형식: {{x:capitalize}}, {{x:start}}, {{x:indefinite}}
섹션 제목: “텍스트 힌트와 라벨 형식: {{x:capitalize}}, {{x:start}}, {{x:indefinite}}”세 힌트가 문자열, enum 또는 종류 라벨, 캐스트 이름, {{occasion.target}}, {{userName}} 같은 string 또는 enum 호스트 토큰을 형식화합니다. :capitalize는 첫 글자를 대문자로 바꿉니다. :start와 :indefinite는 종류의 labels: 항목이 텍스트와 함께 선언할 수 있는 형식을 읽습니다:
entities: spot: members: [cut, den] labels: cut: { text: "the smugglers' cut", start: "The smugglers' cut", indefinite: "a smugglers' cut" } den: ashwraith den{{occasion.target:start}} is quiet.는 cut에 대해 The smugglers' cut is quiet.로 읽힙니다. 형식이 없으면 힌트가 대체 동작을 합니다. :start는 텍스트를 대문자로 만들고(Ashwraith den), :indefinite는 첫 글자가 모음이면 an, 그 밖에는 a를 텍스트 앞에 붙입니다(an ashwraith den). 규칙이 틀리는 단어(an hour, a unicorn)에는 indefinite:를 선언하세요. 라벨 항목은 문자열이거나 { text, start, indefinite }이며 text:가 필수입니다. 다른 키는 모두 E-ENTITY-KIND-SHAPE입니다. IR은 힌트를 플레이스홀더의 format으로, 형식을 종류의 entities[] 항목과 종류로 타입 지정된 상태 경로의 labelForms로 전달하므로 엔진이 지역화할 수 있습니다. 리터럴로 삽입된 컴포넌트 매개변수는 대체 형식을 사용합니다.
힌트는 int 또는 double(또는 int 호스트 토큰)에 대한 ordinal, ordinalWord, cardinalWord, plural과 텍스트에 대한 capitalize, start, indefinite입니다. 그 밖의 힌트 — {{run.floor:roman}}, 잘못 쓴 {{run.day:ordinalword}} — 는 E-CEL-PROFILE입니다. 문자열, enum 또는 bool 경로/def에 숫자 힌트를 쓰거나 {{userName}} 같은 string 또는 enum 호스트 토큰에 숫자 힌트를 쓰면 E-REF-TYPE이고, int, double, bool 또는 int 호스트 토큰에 텍스트 힌트를 써도 같습니다.
지역화 가능한 디렉티브 텍스트 (0.51부터)
섹션 제목: “지역화 가능한 디렉티브 텍스트 (0.51부터)”플러그인 디렉티브는 배너 제목처럼 번역가가 봐야 하는 텍스트를 담을 수 있습니다. 해당 매니페스트가 속성을 type: text로 선언합니다. text 속성은 plugin 레코드로 컴파일되는 디렉티브(lower:도 bridge:도 없음)에서만 허용되며 default:를 가질 수 없습니다. 각 text 속성은 선택지 라벨처럼 번역 단위 하나입니다:
::banner{code="0010" title="Welcome to the hall, {{userName}}" style="card"}- 값 문법.
text값은<choice>라벨과 같은 문법의 따옴표 문자열입니다. 상태 경로,@def, 호스트 토큰의{{…}}보간과 그 수식자를 쓸 수 있습니다.\{{는 리터럴{{를 씁니다(0.52부터, #211. 라벨과 프롬프트 참고). 인라인 텍스트 수식자는 파싱되지 않습니다. 각 보간은 라벨의 보간과 똑같이 검사됩니다.@ref값(title=@greeting)은E-ATTR-TYPE입니다.@def값에는 자체 번역이 없기 때문입니다. code=.text속성이 하나 이상 있는 디렉티브는 콘텐츠 줄처럼code="…"를 받습니다(공백을 제거한 문자열). 그 밖의 디렉티브에서code는 계속E-UNKNOWN-ATTR입니다.text속성 옆에code라는 이름의 속성을 직접 선언하는 디렉티브는 플러그인을 로드할 때 거부됩니다.lineId.text속성의 id는{prefix}~{tag}~{code}~{attr}이며 예는intro~banner~0010~title입니다.lineId템플릿을 거치지 않고, 위치는 절대 정체성이 아닙니다.~가 구분자이므로~를 포함한code는E-LINE-CODE-RESERVED이고, 리터럴 텍스트에~가 들어간 identity 템플릿은E-IDENTITY-TEMPLATE입니다. 안전장치로 프로젝트 전체lute check는 최종lineId가 같은 두 단위(줄, 선택지, 프롬프트, 디렉티브 텍스트)를E-LINE-ID-DUP으로 보고합니다.lineId하나는 번역 하나를 가리키기 때문입니다. 한 범위에서 두::banner호출이code를 공유하면E-DUP-LINE-CODE입니다.- 백필. 태그 없는 디렉티브는 줄과 마찬가지로 범위 안에서 디렉티브 태그별로 센 다음 코드를 받으며, 컴포넌트에서는 컴포넌트 소스 전체에 걸쳐 셉니다.
lute tag는text속성이 있는 태그 없는 디렉티브마다code="…"를 기록합니다. 컴포넌트는 그것을 쓰는 문서들의 스냅숏으로 태그하며, 사용자들이 그 텍스트 디렉티브를 서로 다르게 해석하면E-COMPONENT-TEXT-DIRECTIVE입니다.identity.requireStable이 켜져 있으면code가 없는 그런 디렉티브는W-LINE-CODE-UNTAGGED입니다. - 아티팩트. 레코드의
fields.<attr>는{{…}}마커를 작성한 그대로 둔 원본 언어 문자열을 유지하므로, 지역화를 무시하는 호스트는 이전과 같은 값을 읽습니다.lineId와 플레이스홀더는localized.<attr>에 실리며,code자체는fields에도stamps에도 기록되지 않습니다.text속성이 없는 디렉티브는 0.50과 똑같이 컴파일됩니다.
문자열은 lute loc export, lute loc import, lute compile --locale-bundle로 내보내고 번역해 병합합니다. 디렉티브의 lineId는 code가 작성되어 있을 때만 소스에서 재현할 수 있으므로 먼저 lute tag를 실행하세요.
인라인 텍스트 수식자
섹션 제목: “인라인 텍스트 수식자”줄의 텍스트는 표현을 위해 일부를 마크업할 수 있습니다 — 스타일이 지정된 단어, 일시 정지, 느려진 구절 등입니다. 수식자(modifier)는 콜론, lowerCamel 이름, 그리고 대괄호로 감싼 스팬, 중괄호 속성 목록 또는 둘 다로 이루어집니다:
- 스팬 —
:name[text]는 텍스트를 감쌉니다. - 말단 —
:name{attrs}는 텍스트의 한 지점을 표시합니다. - 속성이 있는 스팬 —
:name[text]{attrs}.
수식자는 콘텐츠 줄의 두 번째 콜론 뒤 텍스트에서만 유효하며, 헤딩, 속성, 태그 텍스트 또는 <choice>의 text에서는 절대 유효하지 않습니다. 코어에는 다음 두 가지가 있습니다:
| 수식자 | 형식 | 의미 |
|---|---|---|
:pause{s=0.5} | 말단 | s초 동안 일시 정지(s 필수, 음이 아닌 값) |
:speed[text]{rate=1.25} | 스팬 | 감싼 텍스트를 rate 속도로 전달(rate 필수, 양수) |
그 밖의 모든 이름은 프로젝트의 textStyle 도메인에 속하는 텍스트 스타일이며, 다른 연출 어휘와 마찬가지로 enums:에서 선언합니다. 스타일은 속성이 없는 스팬입니다. emphasis나 whisper를 어떻게 보일지는 엔진의 역할입니다.
---kind: sceneid: diner.nightpov: fixerenums: textStyle: [emphasis, whisper]state: run.tips: { type: int, default: 3 }---
## The Counter
@mira: You look :emphasis[tired].:pause{s=0.5} Coffee?@fixer: :speed[Just... one more cup.]{rate=0.75}@mira: :whisper[I counted :emphasis[{{run.tips}}] tips tonight.]@fixer: Write \:emphasis[this] to show the markup itself.@fixer: The sign says 10:30 :) and nothing else.아티팩트에서 text는 항상 평문입니다. 수식자 마크업은 제거되고, 이스케이프는 디코딩되며(리터럴 {{는 예외, 아래 참고), {{…}}는 유지됩니다. 수식자를 사용하는 줄에는 활성 styles(바깥쪽부터)와 rate를 담은 텍스트 실행 구간, 그리고 {pause} 말단으로 이루어진 segments도 생깁니다. 수식자가 없는 줄에는 segments가 없습니다:
{"kind": "line", "family": "content", "position": "001-0100", "role": "dialogue", "speaker": "mira", "text": "You look tired. Coffee?", "lineId": "diner.night.mira_0010", "voiceKey": "diner.night.mira-0010", "segments": [{"text": "You look "}, {"text": "tired", "styles": ["emphasis"]}, {"text": "."}, {"pause": 0.5}, {"text": " Coffee?"}]}{"kind": "line", "family": "content", "position": "001-0200", "role": "dialogue", "speaker": "fixer", "text": "Just... one more cup.", "lineId": "diner.night.fixer_0010", "voiceKey": "diner.night.fixer-0010", "segments": [{"text": "Just... one more cup.", "rate": 0.75}]}{"kind": "line", "family": "content", "position": "001-0300", "role": "dialogue", "speaker": "mira", "text": "I counted {{run.tips}} tips tonight.", "lineId": "diner.night.mira_0020", "voiceKey": "diner.night.mira-0020", "placeholders": [{"kind": "path", "path": "run.tips"}], "segments": [{"text": "I counted ", "styles": ["whisper"]}, {"text": "{{run.tips}}", "styles": ["whisper", "emphasis"]}, {"text": " tips tonight.", "styles": ["whisper"]}]}{"kind": "line", "family": "content", "position": "001-0400", "role": "dialogue", "speaker": "fixer", "text": "Write :emphasis[this] to show the markup itself.", "lineId": "diner.night.fixer_0020", "voiceKey": "diner.night.fixer-0020"}위 예제의 규칙은 줄마다 다음과 같습니다:
- 중첩. 스팬은 중첩될 수 있고 보간은 스팬 안에 들어갈 수 있으며
text와placeholders에 그대로 남습니다. 중첩 스타일은 쌓이고(["whisper", "emphasis"]) 중첩된:speed는 가장 안쪽의rate를 사용합니다. 스타일과 rate가 같은 인접 실행 구간은 합쳐집니다. - 이스케이프.
\:는 수식자를 시작하지 않는 리터럴 콜론입니다. 수식자 안에서\[,\],\{,\}및\\는 괄호, 중괄호 또는 백슬래시를 씁니다. 그 밖의 이스케이프는E-TEXT-ESCAPE입니다. - 리터럴
{{.\{{는 리터럴{{를 씁니다.{{바로 앞의 백슬래시가 홀수 개이면 이스케이프되고, 짝수 개이면 마커가 남습니다(\\{{run.a}}는 백슬래시 하나 뒤에 마커). 아티팩트에서는(lute 0.47.1) 리터럴{{를text, segment 실행 구간, 번역 모두에서\{{로 쓰고{{바로 앞의 리터럴 백슬래시는 두 번 씁니다. 그래서Lit \{{run.a}} and {{ run.a }}.는 그text그대로 placeholder 하나를 가지며,run.a가 3이면Lit {{run.a}} and 3.로 렌더링됩니다. 번역에도\{{를 쓸 수 있습니다. - 리터럴 중괄호로 열리는 마커 (0.52, #139, 0.53, #229). 아티팩트에는
{{…}}마커 바로 앞의 리터럴{를 적을 방법이 없습니다. 컴파일된 텍스트가 마커를 중괄호 하나 일찍 열게 됩니다. 그래서 줄 텍스트에서 앞의 리터럴 텍스트가 홀수 개의{로 끝나는 마커는, 이스케이프나 수식자로 어떻게 쓰든({\{{{{run.a}},x{:pause{s=0.5}{{run.a}})E-INTERP-BRACE입니다. 짝수 개는 리터럴{{쌍이므로 그대로 허용됩니다.\{{{{run.a}}는{{3으로 렌더링됩니다. 0.53부터는 표현식 자체가{로 시작하는 마커({{{run.a}},{{{{run.a}})도 리터럴 중괄호 n개 뒤의 마커로 읽어E-INTERP-BRACE로 보고합니다. 줄 텍스트와 라벨 문법을 쓰는 모든 값(선택지·허브 라벨, 프롬프트, 플러그인text속성)에서 마찬가지이며, 0.52는 이를E-CEL-PROFILE로 보고했습니다. 메시지는 중괄호 개수 n을 밝히고 정확히 그 개수에 맞는 두 가지 해결 방법을 알려 줍니다.{{뒤에 공백이 있는{{ {run.a}}는 작성자가 직접 연 마커이므로E-CEL-PROFILE로 남습니다. 이런 마커가 있는 번역은 병합되지 않으며(번역은 소스의 마커를 유지합니다 참고), 끼워 넣은 인자가 중괄호를 끝내는 컴포넌트 줄은::use에서 보고됩니다. 평범한{{로 끝나는 인자도 마찬가지입니다(#221, 컴포넌트 참고). - 평범한 콜론은 텍스트로 남습니다. 콜론 뒤에 이름과
[또는{가 이어질 때만 수식자를 시작하므로10:30과:)에는 이스케이프가 필요 없습니다.
sign.lute:14:20: error [E-INTERP-BRACE] a literal `{` directly before `{{run.a}}` cannot be told apart from the marker in compiled text (lute 0.47.1 §3): put a character between them (`{ {{run.a}}`), or show the brace from a string state path (`{{run.lbrace}}{{run.a}}`, with `run.lbrace` declared `{ type: string, default: "{" }`)Your score: {{{run.a}}(n = 1)의 메시지입니다. {\{{{{run.a}}처럼 중괄호가 세 개이면 메시지는 “literal {{{”라고 하고 \{{{ {{run.a}}와 {{run.lbrace}} 세 개를 제안합니다:
sign.lute:14:24: error [E-INTERP-BRACE] a literal `{{{` directly before `{{run.a}}` cannot be told apart from the marker in compiled text (lute 0.47.1 §3): put a character between them (`\{{{ {{run.a}}`), or show each brace from a string state path (`{{run.lbrace}}{{run.lbrace}}{{run.lbrace}}{{run.a}}`, with `run.lbrace` declared `{ type: string, default: "{" }`)형식이 잘못된 수식자는 E-TEXT-MODIFIER입니다. 여기에는 닫히지 않은 스팬, 코어 수식자도 textStyle 멤버도 아닌 이름, 스타일에 붙인 속성, 위치 인자(:pause{0.5} — s=0.5로 쓰세요)가 포함됩니다:
diner.lute:11:33: error [E-TEXT-MODIFIER] inline modifier span is missing a closing `]`diner.lute:12:10: error [E-TEXT-MODIFIER] `:shout` is neither a core modifier (`pause`, `speed`) nor a member of `textStyle` (it has `emphasis`)diner.lute:13:26: error [E-TEXT-MODIFIER] text style `:emphasis` takes no attributes, got `size` — only the core `pause`/`speed` carry attributes보이스 키는 평문을 비교하므로 줄의 스타일만 바꾸어도 녹음과 충돌하지 않습니다. lute loc export는 소스 마크업을 내보내며, 번역은 동일한 수식자를 전달해야 합니다 — 이름, 형식, 속성이 같아야 하고 순서는 달라도 됩니다 — 그렇지 않으면 병합되지 않습니다(E-L10N-MODIFIERS). 번역된 로케일에는 항상 texts에 평문이 들어가며, 수식자가 있는 줄에는 해당 로케일의 localeSegments도 들어갑니다.
export의 각 행에는 sourceHash도 있습니다 — 줄의 평문(마크업과 이스케이프를 제거한, 컴파일된 text), 선택지 텍스트 또는 프롬프트의 "sha256:<hex>"입니다. 번역 파일에 그대로 두세요. lute loc import가 이를 lineId와 로케일별로 번들(schemaVersion: 2, sourceHashes)에 저장하고, lute compile --locale-bundle는 번역의 바탕이 된 export 이후 줄이 수정되었으면 W-L10N-STALE을 보고합니다 — 다시 내보내 그 번역을 갱신하세요. 갱신하기 전까지 오래된 번역도 그대로 병합됩니다.
번역은 소스의 마커를 유지합니다
섹션 제목: “번역은 소스의 마커를 유지합니다”0.53부터 번역은 수식자와 마찬가지로 소스의 {{…}} 마커도 그대로 보여 주어야 합니다. 소스의 각 마커가 소스에 나온 횟수만큼, 순서는 자유롭게 나와야 합니다. 마커는 양 끝의 공백을 뺀 안쪽 내용으로 비교하므로 {{ run.a }}는 {{run.a}}와 같지만, 안쪽의 공백이나 다른 형식 힌트는 다른 마커가 됩니다({{run.a: ordinal}}은 {{run.a:ordinal}}이 아닙니다). 이 규칙은 줄, 선택지 라벨, 프롬프트, 디렉티브 text 속성에 모두 적용됩니다. 리터럴 중괄호로 열리는 마커이거나 소스의 마커가 아니면 E-L10N-MARKERS입니다. 그 번역은 병합되지 않고 lute compile --locale-bundle이 실패합니다. 줄 번역에 }} 없는 {{가 있으면 파싱되지 않는 인라인 마크업과 마찬가지로 컴파일이 E-L10N-MODIFIERS(E-INTERP-UNTERMINATED를 감쌈)로 실패합니다. lute loc import는 번역된 행만 보므로 파싱되지 않는 마커를 거부하고(줄의 }} 없는 {{도 여기서는 E-L10N-MARKERS) 번들을 쓰지 않습니다. 이 검사가 없으면 런타임이 소스와 짝이 맞지 않는 마커를 그대로 노출합니다. 소스가 Score {{run.a}} now.이고 ko 텍스트가 점수 {{run.zzz}}인 경우:
s.lute:1:1: error [E-L10N-MARKERS] `ko` text for `s.narrator_0010` is not merged: its markers differ from the source's (missing `{{run.a}}`; extra `{{run.zzz}}`): a translation shows each `{{…}}` marker of its source as often as the source does, in any order