Skip to content

Commit 17d228e

Browse files
committed
First iteration on supporting po, xliff and csv/tsv format for translation files
1 parent c232d52 commit 17d228e

39 files changed

Lines changed: 1662 additions & 14 deletions

cpp/include/Loreline.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,20 @@ LORELINE_PUBLIC void Loreline_loadLocaleAsync(
284284
void* completionHandlerData
285285
);
286286

287+
/* Enable or disable runtime support for an alternate translation file format.
288+
*
289+
* By default, only `.<locale>.lor` files are tried by Loreline_loadLocale.
290+
* Call this to opt in to additional formats:
291+
* - "po" — GNU gettext PO (.po)
292+
* - "xliff" — XLIFF 1.2 / 2.x (.xliff, .xlf)
293+
* - "csv" — CSV / TSV (.csv, .tsv)
294+
*
295+
* Unknown names are accepted silently (forward-compat). */
296+
LORELINE_PUBLIC void Loreline_translationFormat(
297+
Loreline_String name,
298+
bool enabled
299+
);
300+
287301
/* Interpreter options — configure custom functions, strict access, translations */
288302
LORELINE_PUBLIC Loreline_InterpreterOptions* Loreline_createOptions(void);
289303
LORELINE_PUBLIC void Loreline_releaseOptions(Loreline_InterpreterOptions* options);

godot/src/loreline_js_bridge.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ static const char LORELINE_JS_BRIDGE[] = R"LORELINE_BRIDGE(
174174
_releaseObj(translationsId);
175175
},
176176
177+
// --- Translation formats (PO, XLIFF, CSV...) ---
178+
// Enable/disable runtime support for an alternate translation file format.
179+
// Names: "po", "xliff", "csv". Unknown names accepted silently.
180+
translationFormat: function(name, enabled) {
181+
Loreline.translationFormat(name, enabled);
182+
},
183+
177184
provideFunctionDone: function(callId) {
178185
var entry = _pendingFunctionDone[callId];
179186
if (entry) {

godot/src/loreline_runtime.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ void Loreline::_bind_methods() {
7474
ClassDB::bind_static_method("Loreline", D_METHOD("shared"), &Loreline::shared);
7575
ClassDB::bind_method(D_METHOD("parse", "source", "file_path", "file_handler"), &Loreline::parse, DEFVAL(""), DEFVAL(Callable()));
7676
ClassDB::bind_method(D_METHOD("load_locale", "locale", "script", "file_path", "file_handler"), &Loreline::load_locale, DEFVAL(""), DEFVAL(Callable()));
77+
ClassDB::bind_method(D_METHOD("translation_format", "name", "enabled"), &Loreline::translation_format);
7778
ClassDB::bind_method(D_METHOD("play", "script", "on_dialogue", "on_choice", "on_finished", "beat_name", "options"), &Loreline::play, DEFVAL(Callable()), DEFVAL(Callable()), DEFVAL(Callable()), DEFVAL(""), DEFVAL(Ref<LorelineOptions>()));
7879
ClassDB::bind_method(D_METHOD("resume", "script", "on_dialogue", "on_choice", "on_finished", "save_data", "beat_name", "options"), &Loreline::resume, DEFVAL(""), DEFVAL(Ref<LorelineOptions>()));
7980
}
@@ -545,6 +546,19 @@ Signal Loreline::load_locale(const String &locale, const Ref<LorelineScript> &sc
545546
#endif
546547
}
547548

549+
void Loreline::translation_format(const String &name, bool enabled) {
550+
#ifdef LORELINE_USE_JS
551+
JavaScriptBridge *js = JavaScriptBridge::get_singleton();
552+
if (!js) return;
553+
String escaped = loreline_escape_js(name);
554+
String js_code = String("_lorelineBridge.translationFormat('") + escaped + "'," + (enabled ? "true" : "false") + ")";
555+
js->eval(js_code, true);
556+
#else
557+
CharString name_utf8 = name.utf8();
558+
Loreline_translationFormat(Loreline_String(name_utf8.get_data()), enabled);
559+
#endif
560+
}
561+
548562
Ref<LorelineInterpreter> Loreline::play(const Ref<LorelineScript> &script, const Callable &on_dialogue, const Callable &on_choice, const Callable &on_finished, const String &beat_name, const Ref<LorelineOptions> &options) {
549563
if (!_initialized) {
550564
UtilityFunctions::push_error("Loreline: not initialized. Add this node to the scene tree first.");

godot/src/loreline_runtime.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ class Loreline : public Node {
124124
// var translations = await loreline.load_locale("fr", script)
125125
Signal load_locale(const String &locale, const Ref<LorelineScript> &script, const String &file_path = "", const Callable &file_handler = Callable());
126126

127+
// Enable or disable runtime support for an alternate translation file
128+
// format. By default only `.<locale>.lor` is tried.
129+
// Known names: "po", "xliff", "csv". Unknown names accepted silently.
130+
void translation_format(const String &name, bool enabled);
131+
127132
Ref<LorelineInterpreter> play(const Ref<LorelineScript> &script, const Callable &on_dialogue = Callable(), const Callable &on_choice = Callable(), const Callable &on_finished = Callable(), const String &beat_name = "", const Ref<LorelineOptions> &options = Ref<LorelineOptions>());
128133
Ref<LorelineInterpreter> resume(const Ref<LorelineScript> &script, const Callable &on_dialogue, const Callable &on_choice, const Callable &on_finished, const String &save_data, const String &beat_name = "", const Ref<LorelineOptions> &options = Ref<LorelineOptions>());
129134

js/loreline.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,22 @@ export class Loreline {
297297
callback?: (translations: Translations) => void
298298
): Translations | null;
299299

300+
/**
301+
* Enable or disable runtime support for an alternate translation file format.
302+
*
303+
* By default only `.<locale>.lor` files are tried by `loadLocale`. Call
304+
* this to opt in to additional formats:
305+
* - `"po"` — GNU gettext PO (`.po`)
306+
* - `"xliff"` — XLIFF 1.2 / 2.x (`.xliff`, `.xlf`)
307+
* - `"csv"` — CSV / TSV (`.csv`, `.tsv`)
308+
*
309+
* Unknown names are accepted silently (forward-compat for future formats).
310+
*
311+
* @param name The format identifier (see above)
312+
* @param enabled True to enable the format, false to disable
313+
*/
314+
static translationFormat(name: string, enabled: boolean): void;
315+
300316
/**
301317
* Prints a parsed script back into Loreline source code.
302318
*

js/test-runner.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,11 @@ async function main(): Promise<void> {
289289
process.exit(1);
290290
}
291291

292+
// Test fixtures exercise every supported translation format.
293+
Loreline.translationFormat('po', true);
294+
Loreline.translationFormat('xliff', true);
295+
Loreline.translationFormat('csv', true);
296+
292297
for (const filePath of testFiles) {
293298
const rawContent: string = readFileSync(filePath, 'utf-8');
294299
const testItems: TestItem[] = extractTests(rawContent);

jvm/src/loreline/Loreline.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,21 @@ public static Object extractTranslations(Script script) {
124124
return loreline.runtime.Loreline.extractTranslations(script.runtimeScript);
125125
}
126126

127+
/**
128+
* Enable or disable runtime support for an alternate translation file format.
129+
*
130+
* By default only `.<locale>.lor` files are tried by loadLocale. Call this
131+
* to opt in to additional formats. Known names:
132+
* - "po" — GNU gettext PO (.po)
133+
* - "xliff" — XLIFF 1.2 / 2.x (.xliff, .xlf)
134+
* - "csv" — CSV / TSV (.csv, .tsv)
135+
*
136+
* Unknown names are accepted silently (forward-compat).
137+
*/
138+
public static void translationFormat(String name, boolean enabled) {
139+
loreline.runtime.Loreline.translationFormat(name, enabled);
140+
}
141+
127142
/**
128143
* Loads translations for a specific locale.
129144
* Walks the script's full import tree and loads `.<locale>.lor` files for each.

src/loreline/Loreline.hx

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,24 @@ class Loreline {
140140
throw new Error("Cannot load locale: handleFile is required");
141141
}
142142

143-
// Determine which Loreline extension we're working with: prefer the source script's
144-
// extension when known, otherwise infer from filePath. The result is the canonical
145-
// lowercase ".lor" or ".lor.txt" used to build translation file names.
146-
final ext = (script.filePath != null && Imports.endsWithLorTxt(script.filePath))
147-
? '.lor.txt'
148-
: Imports.lorExtension(filePath);
143+
// Wrap to enable alternate translation file formats (PO, XLIFF, CSV/TSV).
144+
// Pass-through if no format was enabled via Loreline.translationFormat();
145+
// otherwise the wrap tries each enabled format as a sibling and converts
146+
// the first match to a synthesized .lor translation body before handing
147+
// it back to loadLocale.
148+
handleFile = loreline.translation.TranslationFormats.wrap(handleFile, locale);
149+
150+
// Determine which Loreline extension we're working with for translation files.
151+
// By default, always ".lor" — ".lor.txt" translations are only considered when
152+
// built with -D loreline_lor_txt (which restores the previous behavior of
153+
// matching the source script's extension).
154+
final ext = #if loreline_lor_txt
155+
(script.filePath != null && Imports.endsWithLorTxt(script.filePath))
156+
? '.lor.txt'
157+
: Imports.lorExtension(filePath);
158+
#else
159+
'.lor';
160+
#end
149161

150162
// Determine where to look for translation files.
151163
// If filePath ends with .lor / .lor.txt, treat its directory as the lookup base.
@@ -357,6 +369,24 @@ class Loreline {
357369
return AstUtils.extractTranslations(script);
358370
}
359371

372+
/**
373+
* Enable or disable runtime support for an alternate translation file format.
374+
*
375+
* By default, only `.<locale>.lor` files are tried by `loadLocale`. Call this
376+
* to opt in to additional formats. Known names:
377+
* - `"po"` — GNU gettext PO (`.po`)
378+
* - `"xliff"` — XLIFF 1.2 / 2.x (`.xliff`, `.xlf`)
379+
* - `"csv"` — CSV / TSV (`.csv`, `.tsv`)
380+
*
381+
* Unknown names are accepted silently (forward-compat for future formats).
382+
*
383+
* @param name The format identifier (see above)
384+
* @param enabled True to enable the format, false to disable
385+
*/
386+
public static function translationFormat(name:String, enabled:Bool):Void {
387+
loreline.translation.TranslationFormats.translationFormat(name, enabled);
388+
}
389+
360390
/**
361391
* Generates a translation file body for the given source `script`.
362392
*
@@ -372,8 +402,20 @@ class Loreline {
372402
* @param existing (optional) Existing translations to preserve when merging
373403
* @return The translation file body as a string, ready to write to disk
374404
*/
375-
public static function generateTranslationFile(script:Script, ?existing:Map<String, NStringLiteral>):String {
376-
return AstUtils.generateTranslationFile(script, existing, new Printer());
405+
public static function generateTranslationFile(script:Script, ?existing:Map<String, NStringLiteral>, ?format:String, ?locale:String):String {
406+
if (format == null || format == "lor") {
407+
return AstUtils.generateTranslationFile(script, existing, new Printer());
408+
}
409+
if (locale == null || locale == "") {
410+
throw new Error("A locale is required when generating a translation file in format '" + format + "'");
411+
}
412+
return switch (format) {
413+
case "po": loreline.translation.PoTranslation.fromScript(script, existing, locale);
414+
case "xliff": loreline.translation.XliffTranslation.fromScript(script, existing, locale);
415+
case "csv": loreline.translation.CsvTranslation.fromScript(script, existing, locale);
416+
case "tsv": loreline.translation.CsvTranslation.tsvFromScript(script, existing, locale);
417+
case _: throw new Error("Unknown translation file format: '" + format + "'");
418+
}
377419
}
378420

379421
/**

src/loreline/cli/Cli.hx

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,11 @@ class Cli {
228228
var fileCount = 0;
229229
var fileFailCount = 0;
230230

231+
// Test fixtures exercise every supported translation format.
232+
Loreline.translationFormat("po", true);
233+
Loreline.translationFormat("xliff", true);
234+
Loreline.translationFormat("csv", true);
235+
231236
if (FileSystem.exists(path) && FileSystem.isDirectory(path)) {
232237
var dir = path;
233238
for (file in FileSystem.readDirectory(dir)) {
@@ -615,6 +620,17 @@ class Cli {
615620
final clearIds = argFlag(args, "clear");
616621
final lang = argValue(args, "lang", !clearIds);
617622
final generateIds = argFlag(args, "auto-ids");
623+
var format = argValue(args, "format", false);
624+
if (format == null || format == "") format = "lor";
625+
if (format != "lor" && format != "po" && format != "xliff" && format != "csv" && format != "tsv") {
626+
fail('Unsupported translation format: $format (expected one of: lor, po, xliff, csv, tsv)');
627+
}
628+
629+
// Enable alternate translation formats so that an existing file can be
630+
// read back as the merge baseline.
631+
Loreline.translationFormat("po", true);
632+
Loreline.translationFormat("xliff", true);
633+
Loreline.translationFormat("csv", true);
618634

619635
if (!FileSystem.exists(file) || FileSystem.isDirectory(file))
620636
fail('Invalid file: $file');
@@ -636,20 +652,25 @@ class Cli {
636652
script = Loreline.parse(content, file, handleFile);
637653
}
638654

639-
final basePath = file.substring(0, file.length - 4);
640-
final translationPath = basePath + "." + lang + ".lor";
655+
final basePath = file.uSubstring(0, file.uLength() - 4);
656+
final translationPath = basePath + "." + lang + "." + format;
641657

642658
var existingTranslations:Map<String, Node.NStringLiteral> = null;
643659
if (FileSystem.exists(translationPath)) {
644660
final transContent = File.getContent(translationPath);
645661
if (transContent.trim().length > 0) {
646-
final transScript = Loreline.parse(transContent, translationPath, handleFile);
647-
if (transScript != null)
648-
existingTranslations = AstUtils.extractTranslations(transScript);
662+
final lorBody = (format == "lor")
663+
? transContent
664+
: convertExistingToLor(transContent, format, lang);
665+
if (lorBody != null && lorBody.trim().length > 0) {
666+
final transScript = Loreline.parse(lorBody, translationPath, handleFile);
667+
if (transScript != null)
668+
existingTranslations = AstUtils.extractTranslations(transScript);
669+
}
649670
}
650671
}
651672

652-
final output = AstUtils.generateTranslationFile(script, existingTranslations, new Printer());
673+
final output = Loreline.generateTranslationFile(script, existingTranslations, format, lang);
653674
File.saveContent(translationPath, output);
654675

655676
print('Translation file ' + (existingTranslations != null ? 'updated' : 'created') + ': ' + translationPath);
@@ -669,6 +690,16 @@ class Cli {
669690

670691
}
671692

693+
function convertExistingToLor(content:String, format:String, locale:String):String {
694+
return switch (format) {
695+
case "po": loreline.translation.PoTranslation.toLoreline(content, locale);
696+
case "xliff": loreline.translation.XliffTranslation.toLoreline(content, locale);
697+
case "csv": loreline.translation.CsvTranslation.toLoreline(content, locale);
698+
case "tsv": loreline.translation.CsvTranslation.tsvToLoreline(content, locale);
699+
case _: null;
700+
}
701+
}
702+
672703
function play(file:String) {
673704

674705
print("");

src/loreline/lib/linc/linc_Loreline.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,6 +1241,27 @@ LORELINE_PUBLIC Loreline_Translations* Loreline_loadLocale(
12411241
return slot.result;
12421242
}
12431243

1244+
/* ── Translation formats ────────────────────────────────────────────────── */
1245+
1246+
static LORELINE_NOINLINE void Loreline_translationFormat_hx(
1247+
Loreline_String name,
1248+
bool enabled
1249+
) {
1250+
LORELINE_HX_BEGIN
1251+
::String hxName = linc_toHxString(name);
1252+
::loreline::Loreline_obj::translationFormat(hxName, enabled);
1253+
LORELINE_HX_END
1254+
}
1255+
1256+
LORELINE_PUBLIC void Loreline_translationFormat(
1257+
Loreline_String name,
1258+
bool enabled
1259+
) {
1260+
LORELINE_BEGIN_CALL
1261+
Loreline_translationFormat_hx(name, enabled);
1262+
LORELINE_END_CALL
1263+
}
1264+
12441265
/* ── Play ───────────────────────────────────────────────────────────────── */
12451266

12461267
static LORELINE_NOINLINE void Loreline_play_hx(

0 commit comments

Comments
 (0)