changeset 241:76afe639fd72

misc: add support for NLS, closes #22510 @4h While here cleanup the path functions in sys.c/sys.h
author David Demelier <markand@malikania.fr>
date Sat, 28 Nov 2020 18:00:05 +0100
parents d7e5e02f70a1
children 4c24604efcab
files CMakeLists.txt cmake/MolkoBuildTranslations.cmake cmake/MolkoDefineExecutable.cmake cmake/MolkoDefineLibrary.cmake examples/example-action/main.c examples/example-animation/main.c examples/example-audio/main.c examples/example-battle/main.c examples/example-cursor/main.c examples/example-debug/main.c examples/example-drawable/main.c examples/example-font/main.c examples/example-gridmenu/main.c examples/example-label/main.c examples/example-map/main.c examples/example-message/main.c examples/example-sprite/main.c examples/example-trace/main.c examples/example-ui/main.c libadventure/adventure/molko.c libcore/CMakeLists.txt libcore/core/core.c libcore/core/core.h libcore/core/core_p.h libcore/core/panic.c libcore/core/save.c libcore/core/sys.c libcore/core/sys.h libcore/core/sysconfig.h.in libcore/core/translate.c libcore/core/translate.h libcore/nls/fr.po libcore/nls/libcore.pot librpg/CMakeLists.txt librpg/nls/fr.po librpg/nls/librpg.pot librpg/rpg/battle-bar.c librpg/rpg/battle-state-lost.c librpg/rpg/battle-state-victory.c librpg/rpg/map-file.c librpg/rpg/message.c librpg/rpg/rpg.c librpg/rpg/rpg_p.h librpg/rpg/tileset-file.c libui/CMakeLists.txt libui/nls/fr.po libui/nls/libui.pot libui/ui/button.c libui/ui/gridmenu.c libui/ui/ui.c libui/ui/ui_p.h tests/test-map.c tests/test-tileset.c
diffstat 53 files changed, 1058 insertions(+), 200 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Fri Nov 27 21:34:07 2020 +0100
+++ b/CMakeLists.txt	Sat Nov 28 18:00:05 2020 +0100
@@ -24,6 +24,7 @@
 set(CMAKE_C_EXTENSIONS Off)
 set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
 
+option(MOLKO_WITH_NLS "Enable native language support" On)
 option(MOLKO_WITH_DOC "Enable documentation (requires doxygen, doxybook2 and mkdocs)" On)
 option(MOLKO_WITH_TESTS "Enable unit tests" On)
 option(MOLKO_WITH_EXAMPLES "Enable build of examples" On)
@@ -41,6 +42,7 @@
 
 find_package(Jansson REQUIRED)
 find_package(SDL2 REQUIRED COMPONENTS image mixer ttf)
+find_package(Intl)
 
 add_subdirectory(cmake)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cmake/MolkoBuildTranslations.cmake	Sat Nov 28 18:00:05 2020 +0100
@@ -0,0 +1,113 @@
+#
+# MolkoBuildTranslations.cmake -- CMake build system for molko
+#
+# Copyright (c) 2020 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+if (MOLKO_WITH_NLS)
+	find_program(XGETTEXT_EXE xgettext)
+	find_program(MSGMERGE_EXE msgmerge)
+	find_program(MSGFMT_EXE msgfmt)
+endif ()
+
+if (MOLKO_WITH_NLS AND XGETTEXT_EXE AND MSGMERGE_EXE)
+	macro(molko_build_translations)
+		set(options)
+		set(oneValueArgs TARGET OUTPUTS)
+		set(multiValueArgs SOURCES TRANSLATIONS)
+
+		cmake_parse_arguments(NLS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+		if (NOT NLS_OUTPUTS)
+			message(FATAL_ERROR "Missing OUTPUTS argument")
+		endif ()
+		if (NOT NLS_TARGET)
+			message(FATAL_ERROR "Missing TARGET argument")
+		endif ()
+		if (NOT NLS_SOURCES)
+			message(FATAL_ERROR "Missing SOURCES argument")
+		endif ()
+
+		# Remove non C files.
+		list(FILTER NLS_SOURCES INCLUDE REGEX "\\.[ch$]")
+		set(pot ${CMAKE_CURRENT_SOURCE_DIR}/nls/${NLS_TARGET}.pot)
+
+		# Generate .pot file.
+		add_custom_target(
+			${NLS_TARGET}-pot
+			ALL
+			VERBATIM
+			COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_SOURCE_DIR}/nls
+			COMMAND ${XGETTEXT_EXE} -cj -k_ -kN_ -LC -s -o ${pot} ${NLS_SOURCES}
+			COMMENT "Generating reference translation ${pot}"
+		)
+		set_target_properties(${NLS_TARGET}-pot PROPERTIES FOLDER translations)
+
+		# For every translation create a msgmerge target and output file.
+		set(outputs)
+
+		foreach (t ${NLS_TRANSLATIONS})
+			set(po ${CMAKE_CURRENT_SOURCE_DIR}/nls/${t}.po)
+			set(mo ${CMAKE_CURRENT_BINARY_DIR}/${t}.mo)
+
+			if (NOT EXISTS ${po})
+				message(WARNING "Missing translation ${po}")
+			endif ()
+
+			add_custom_target(
+				${NLS_TARGET}-po-${t}
+				VERBATIM
+				DEPENDS ${NLS_TARGET}-pot
+				COMMAND ${MSGMERGE_EXE} --backup=off -U ${po} ${pot}
+				COMMENT "Merging translation in ${po}"
+			)
+			set_target_properties(${NLS_TARGET}-po-${t} PROPERTIES FOLDER translations)
+
+			list(APPEND po-targets ${NLS_TARGET}-po-${t})
+
+			# Finally generate a .mo output from po file.
+			add_custom_command(
+				OUTPUT ${mo}
+				VERBATIM
+				COMMAND ${MSGFMT_EXE} -o ${mo} ${po}
+				DEPENDS ${po}
+				COMMENT "Generating translation binary ${mo}"
+			)
+
+			list(APPEND outputs ${mo})
+
+			# TODO: naming should be changed maybe.
+			install(
+				FILES ${mo}
+				DESTINATION ${CMAKE_INSTALL_LOCALEDIR}/${t}/LC_MESSAGES
+				RENAME mlk-${NLS_TARGET}.mo
+			)
+		endforeach ()
+
+		set(${NLS_OUTPUTS} ${outputs})
+		source_group("mo" FILES ${outputs})
+
+		add_custom_target(
+			${NLS_TARGET}-po
+			DEPENDS ${po-targets}
+			COMMENT "Merging all po files"
+		)
+
+		set_target_properties(${NLS_TARGET}-po PROPERTIES FOLDER translations)
+	endmacro()
+else ()
+	function(molko_build_translations)
+	endfunction()
+endif ()
--- a/cmake/MolkoDefineExecutable.cmake	Fri Nov 27 21:34:07 2020 +0100
+++ b/cmake/MolkoDefineExecutable.cmake	Sat Nov 28 18:00:05 2020 +0100
@@ -17,12 +17,13 @@
 #
 
 include(${CMAKE_CURRENT_LIST_DIR}/MolkoBuildAssets.cmake)
+include(${CMAKE_CURRENT_LIST_DIR}/MolkoBuildTranslations.cmake)
 include(${CMAKE_CURRENT_LIST_DIR}/MolkoSetCompilerFlags.cmake)
 
 function(molko_define_executable)
 	set(options)
 	set(oneValueArgs FOLDER TARGET)
-	set(multiValueArgs ASSETS FLAGS INCLUDES LIBRARIES SOURCES)
+	set(multiValueArgs ASSETS FLAGS INCLUDES LIBRARIES SOURCES TRANSLATIONS)
 
 	cmake_parse_arguments(EXE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
 
@@ -35,6 +36,14 @@
 
 	molko_build_assets("${EXE_ASSETS}" OUTPUTS)
 
+	if (EXE_TRANSLATIONS)
+		molko_build_translations(
+			TARGET ${EXE_TARGET}
+			TRANSLATIONS ${EXE_TRANSLATIONS}
+			SOURCES ${EXE_SOURCES}
+		)
+	endif ()
+
 	add_executable(${EXE_TARGET} ${EXE_SOURCES} ${OUTPUTS})
 	target_compile_definitions(${EXE_TARGET} PRIVATE ${EXE_FLAGS})
 	target_include_directories(
--- a/cmake/MolkoDefineLibrary.cmake	Fri Nov 27 21:34:07 2020 +0100
+++ b/cmake/MolkoDefineLibrary.cmake	Sat Nov 28 18:00:05 2020 +0100
@@ -67,7 +67,7 @@
 function(molko_define_library)
 	set(options EXTERNAL)
 	set(oneValueArgs FOLDER TARGET TYPE)
-	set(multiValueArgs ASSETS LIBRARIES MAPS PRIVATE_FLAGS PRIVATE_INCLUDES PUBLIC_FLAGS PUBLIC_INCLUDES TILESETS SOURCES)
+	set(multiValueArgs ASSETS LIBRARIES MAPS PRIVATE_FLAGS PRIVATE_INCLUDES PUBLIC_FLAGS PUBLIC_INCLUDES TILESETS SOURCES TRANSLATIONS)
 
 	cmake_parse_arguments(LIB "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
 
@@ -99,6 +99,15 @@
 				${LIB_PUBLIC_INCLUDES}
 		)
 	else ()
+		if (LIB_TRANSLATIONS)
+			molko_build_translations(
+				TARGET ${LIB_TARGET}
+				TRANSLATIONS ${LIB_TRANSLATIONS}
+				SOURCES ${LIB_SOURCES}
+				OUTPUTS NLS_OUTPUTS
+			)
+		endif ()
+
 		add_library(
 			${LIB_TARGET}
 			${LIB_TYPE}
@@ -106,7 +115,9 @@
 			${ASSETS_OUTPUTS}
 			${MAPS_OUTPUTS}
 			${TILESETS_OUTPUTS}
+			${NLS_OUTPUTS}
 		)
+
 		target_include_directories(
 			${LIB_TARGET}
 			PRIVATE
--- a/examples/example-action/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-action/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -315,7 +315,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init() || !rpg_init())
+	if (!core_init("fr.malikania", "actions") || !ui_init() || !rpg_init())
 		panic();
 	if (!window_open("Example - Action", W, H))
 		panic();
--- a/examples/example-animation/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-animation/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -53,7 +53,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init())
+	if (!core_init("fr.malikania", "animation") || !ui_init())
 		panic();
 	if (!window_open("Example - Animation", W, H))
 		panic();
--- a/examples/example-audio/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-audio/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -59,7 +59,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init())
+	if (!core_init("fr.malikania", "audio") || !ui_init())
 		panic();
 	if (!window_open("Example - Audio", W, H))
 		panic();
--- a/examples/example-battle/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-battle/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -146,7 +146,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init() || !rpg_init())
+	if (!core_init("fr.malikania", "battle") || !ui_init() || !rpg_init())
 		panic();
 	if (!window_open("Example - Battle", W, H))
 		panic();
--- a/examples/example-cursor/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-cursor/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -48,7 +48,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init())
+	if (!core_init("fr.malikania", "cursor") || !ui_init())
 		panic();
 	if (!window_open("Example - Cursor", W, H))
 		panic();
--- a/examples/example-debug/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-debug/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -38,7 +38,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init())
+	if (!core_init("fr.malikania", "debug") || !ui_init())
 		panic();
 	if (!window_open("Example - Debug", W, H))
 		panic();
--- a/examples/example-drawable/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-drawable/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -71,7 +71,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init())
+	if (!core_init("fr.malikania", "drawable") || !ui_init())
 		panic();
 	if (!window_open("Example - Drawable", W, H))
 		panic();
--- a/examples/example-font/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-font/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -52,7 +52,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init())
+	if (!core_init("fr.malikania", "font") || !ui_init())
 		panic();
 	if (!window_open("Example - Font", W, H))
 		panic();
--- a/examples/example-gridmenu/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-gridmenu/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -39,7 +39,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init())
+	if (!core_init("fr.malikania", "grid-menu") || !ui_init())
 		panic();
 	if (!window_open("Example - Grid menu", W, H))
 		panic();
--- a/examples/example-label/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-label/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -102,7 +102,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init())
+	if (!core_init("fr.malikania", "label") || !ui_init())
 		panic();
 	if (!window_open("Example - Label", W, H))
 		panic();
--- a/examples/example-map/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-map/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -272,7 +272,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init())
+	if (!core_init("fr.malikania", "map") || !ui_init())
 		panic();
 	if (!window_open("Example - Map", W, H))
 		panic();
--- a/examples/example-message/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-message/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -44,7 +44,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init() || !rpg_init())
+	if (!core_init("fr.malikania", "message") || !ui_init() || !rpg_init())
 		panic();
 	if (!window_open("Example - Message", W, H))
 		panic();
--- a/examples/example-sprite/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-sprite/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -63,7 +63,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init())
+	if (!core_init("fr.malikania", "sprite") || !ui_init())
 		panic();
 	if (!window_open("Example - Sprite", W, H))
 		panic();
--- a/examples/example-trace/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-trace/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -38,7 +38,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init())
+	if (!core_init("fr.malikania", "trace") || !ui_init())
 		panic();
 	if (!window_open("Example - Trace", W, H))
 		panic();
--- a/examples/example-ui/main.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/examples/example-ui/main.c	Sat Nov 28 18:00:05 2020 +0100
@@ -129,7 +129,7 @@
 static void
 init(void)
 {
-	if (!core_init() || !ui_init())
+	if (!core_init("fr.malikania", "ui") || !ui_init())
 		panic();
 	if (!window_open("Example - UI", W, H))
 		panic();
--- a/libadventure/adventure/molko.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/libadventure/adventure/molko.c	Sat Nov 28 18:00:05 2020 +0100
@@ -80,7 +80,7 @@
 void
 molko_init(void)
 {
-	if (!core_init() || !ui_init() || !rpg_init())
+	if (!core_init("fr.malikania", "molko") || !ui_init() || !rpg_init())
 		panic();
 	if (!window_open("Molko's Adventure", WINDOW_WIDTH, WINDOW_HEIGHT))
 		panic();
--- a/libcore/CMakeLists.txt	Fri Nov 27 21:34:07 2020 +0100
+++ b/libcore/CMakeLists.txt	Sat Nov 28 18:00:05 2020 +0100
@@ -29,6 +29,11 @@
 )
 
 set(
+	PO
+	${libcore_SOURCE_DIR}/nls/fr.po
+)
+
+set(
 	SOURCES
 	${libcore_SOURCE_DIR}/core/action.c
 	${libcore_SOURCE_DIR}/core/action.h
@@ -41,6 +46,7 @@
 	${libcore_SOURCE_DIR}/core/color.h
 	${libcore_SOURCE_DIR}/core/core.c
 	${libcore_SOURCE_DIR}/core/core.h
+	${libcore_SOURCE_DIR}/core/core_p.h
 	${libcore_SOURCE_DIR}/core/drawable.c
 	${libcore_SOURCE_DIR}/core/drawable.h
 	${libcore_SOURCE_DIR}/core/error.c
@@ -76,11 +82,14 @@
 	${libcore_SOURCE_DIR}/core/state.h
 	${libcore_SOURCE_DIR}/core/sys.c
 	${libcore_SOURCE_DIR}/core/sys.h
+	${libcore_SOURCE_DIR}/core/sysconfig.h.in
 	${libcore_SOURCE_DIR}/core/texture.c
 	${libcore_SOURCE_DIR}/core/texture.h
 	${libcore_SOURCE_DIR}/core/texture_p.h
 	${libcore_SOURCE_DIR}/core/trace.c
 	${libcore_SOURCE_DIR}/core/trace.h
+	${libcore_SOURCE_DIR}/core/translate.c
+	${libcore_SOURCE_DIR}/core/translate.h
 	${libcore_SOURCE_DIR}/core/util.c
 	${libcore_SOURCE_DIR}/core/util.h
 	${libcore_SOURCE_DIR}/core/wait.c
@@ -90,13 +99,25 @@
 	${libcore_SOURCE_DIR}/core/window_p.h
 )
 
+configure_file(
+	${libcore_SOURCE_DIR}/core/sysconfig.h.in
+	${libcore_BINARY_DIR}/sysconfig.h
+)
+
 check_library_exists(m sqrt "" LIBM)
 
+if (MOLKO_WITH_NLS AND Intl_FOUND)
+	list(APPEND LIBS ${Intl_LIBRARIES})
+	list(APPEND INCS ${Intl_INCLUDE_DIRS})
+endif ()
+
 molko_define_library(
 	TARGET libcore
-	SOURCES ${SOURCES} ${SQL_ASSETS}
+	SOURCES ${SOURCES} ${SQL_ASSETS} ${PO}
 	ASSETS ${SQL_ASSETS}
+	TRANSLATIONS fr
 	LIBRARIES
+		${LIBS}
 		$<$<BOOL:${LIBM}>:m>
 		libsqlite
 		SDL2::SDL2
@@ -110,8 +131,11 @@
 	PUBLIC_FLAGS
 		_XOPEN_SOURCE=700
 	PUBLIC_INCLUDES
+		${INCS}
 		$<BUILD_INTERFACE:${libcore_SOURCE_DIR}>
+		$<BUILD_INTERFACE:${libcore_BINARY_DIR}>
 )
 
+source_group(assets/sql FILES ${SQL_ASSETS})
 source_group(core FILES ${SOURCES})
-source_group(core/assets/sql FILES ${SQL_ASSETS})
+source_group(nls FILES ${PO})
--- a/libcore/core/core.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/libcore/core/core.c	Sat Nov 28 18:00:05 2020 +0100
@@ -1,5 +1,5 @@
 /*
- * core.c -- libcore convenient header
+ * core.c -- libcore main entry
  *
  * Copyright (c) 2020 David Demelier <markand@malikania.fr>
  *
@@ -16,19 +16,29 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <assert.h>
 #include <stddef.h>
 #include <stdlib.h>
 #include <time.h>
 
 #include "core.h"
 #include "sys.h"
+#include "translate.h"
 
 bool
-core_init(void)
+core_init(const char *organization, const char *name)
 {
+	assert(organization);
+	assert(name);
+
 	srand(time(NULL));
 
-	return sys_init();
+	if (!sys_init(organization, name))
+		return false;
+
+	translate_init("mlk-libcore");
+
+	return true;
 }
 
 void
--- a/libcore/core/core.h	Fri Nov 27 21:34:07 2020 +0100
+++ b/libcore/core/core.h	Sat Nov 28 18:00:05 2020 +0100
@@ -1,5 +1,5 @@
 /*
- * core.h -- libcore convenient header
+ * core.c -- libcore main entry
  *
  * Copyright (c) 2020 David Demelier <markand@malikania.fr>
  *
@@ -21,7 +21,7 @@
 
 /**
  * \file core.h
- * \brief libcore convenient header.
+ *\brief libcore convenient header.
  */
 
 #include <stdbool.h>
@@ -29,10 +29,14 @@
 /**
  * Initialize the core library.
  *
+ * \pre organization != NULL
+ * \pre name != NULL
+ * \param organization the name of the organization
+ * \param name the game name
  * \return False on errors.
  */
 bool
-core_init(void);
+core_init(const char *organization, const char *name);
 
 /**
  * Close the core library.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libcore/core/core_p.h	Sat Nov 28 18:00:05 2020 +0100
@@ -0,0 +1,31 @@
+/*
+ * core_p -- libcore private definitions
+ *
+ * Copyright (c) 2020 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MOLKO_CORE_CORE_P_H
+#define MOLKO_CORE_CORE_P_H
+
+#include "sysconfig.h"
+
+#if defined(MOLKO_WITH_NLS)
+#       include <libintl.h>
+#       define _(s) dgettext("mlk-libcore", s)
+#else
+#       define _(s)
+#endif
+
+#endif /* !MOLKO_CORE_CORE_P_H */
--- a/libcore/core/panic.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/libcore/core/panic.c	Sat Nov 28 18:00:05 2020 +0100
@@ -20,13 +20,14 @@
 #include <stdio.h>
 #include <stdlib.h>
 
+#include "core_p.h"
 #include "error.h"
 #include "panic.h"
 
 static void
 terminate(void)
 {
-	fprintf(stderr, "abort: %s\n", error());
+	fprintf(stderr, _("abort: %s\n"), error());
 	abort();
 	exit(1);
 }
@@ -72,6 +73,6 @@
 	 * This should not happen, if it does it means the user did not fully
 	 * satisfied the constraint of panic_handler.
 	 */
-	fprintf(stderr, "abort: panic handler returned");
+	fprintf(stderr, _("abort: panic handler returned\n"));
 	exit(1);
 }
--- a/libcore/core/save.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/libcore/core/save.c	Sat Nov 28 18:00:05 2020 +0100
@@ -28,6 +28,7 @@
 #include <core/assets/sql/property-remove.h>
 #include <core/assets/sql/property-set.h>
 
+#include "core_p.h"
 #include "error.h"
 #include "save.h"
 #include "sys.h"
@@ -46,6 +47,12 @@
 	return true;
 }
 
+static const char *
+path(unsigned int idx)
+{
+	return pprintf("%s/%u", sys_dir(SYS_DIR_SAVE), idx);
+}
+
 static bool
 execu(struct save *db, const unsigned char *sql)
 {
@@ -57,7 +64,7 @@
 {
 	assert(db);
 
-	return save_open_path(db, sys_savepath(idx), mode);
+	return save_open_path(db, path(idx), mode);
 }
 
 bool
@@ -75,7 +82,7 @@
 	for (size_t i = 0; i < NELEM(table); ++i) {
 		if (!save_get_property(db, &table[i].prop)) {
 			sqlite3_close(db->handle);
-			return errorf("database not initialized correctly");
+			return errorf(_("database not initialized correctly"));
 		}
 
 		*table[i].date = strtoull(table[i].prop.value, NULL, 10);
@@ -165,7 +172,7 @@
 	switch (sqlite3_step(stmt)) {
 	case SQLITE_DONE:
 		/* Not found. */
-		ret = errorf("property '%s' was not found", prop->key);
+		ret = errorf(_("property '%s' was not found"), prop->key);
 		break;
 	case SQLITE_ROW:
 		/* Found. */
--- a/libcore/core/sys.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/libcore/core/sys.c	Sat Nov 28 18:00:05 2020 +0100
@@ -16,89 +16,76 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include "sysconfig.h"
+
 #include <assert.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <limits.h>
 
+#if defined(_WIN32)
+#       include <shlwapi.h>
+#endif
+
 #include <SDL.h>
 #include <SDL_image.h>
 #include <SDL_mixer.h>
 #include <SDL_ttf.h>
 
-#if !defined(_WIN32)            /* Assuming POSIX */
-#	include <sys/types.h>
-#	include <dirent.h>
-#endif
-
 #include "error.h"
 #include "sound.h"
 #include "sys.h"
 
-#if defined(_WIN32)
-
-static void
-determine(char path[], size_t pathlen)
-{
-	char *base = SDL_GetBasePath();
+static struct {
+	char organization[128];
+	char name[128];
+} info = {
+	.organization = "fr.malikania",
+	.name = "molko"
+};
 
-	/* On Windows, the data hierarchy is the same as the project. */
-	snprintf(path, pathlen, "%sassets", base);
-	SDL_free(base);
-}
+static const char *paths[] = {
+	[SYS_DIR_BIN]           = MOLKO_BINDIR,
+	[SYS_DIR_DATA]          = MOLKO_DATADIR,
+	[SYS_DIR_LOCALE]        = MOLKO_LOCALEDIR
+};
 
-#else                           /* Assuming POSIX */
+static const char *abspaths[] = {
+	[SYS_DIR_BIN]           = MOLKO_ABS_BINDIR,
+	[SYS_DIR_DATA]          = MOLKO_ABS_DATADIR,
+	[SYS_DIR_LOCALE]        = MOLKO_ABS_LOCALEDIR
+};
+
+#if defined(_WIN32)
 
 static bool
 is_absolute(const char *path)
 {
-	assert(path);
-
-	return path[0] == '/';
+	return !PathIsRelativeA(path);
 }
 
-static void
-determine(char path[], size_t pathlen)
-{
-	char localassets[PATH_MAX];
-	char *base = SDL_GetBasePath();
-	DIR *dp;
-
-	/* Try assets directory where executable lives. */
-	snprintf(localassets, sizeof (localassets), "%sassets", base);
+#else
 
-	if ((dp = opendir(localassets))) {
-		snprintf(path, pathlen, "%sassets", base);
-		closedir(dp);
-	} else {
-		/* We are not in the project source directory. */
-		if (is_absolute(SHAREDIR)) {
-			/* SHAREDIR is absolute */
-			snprintf(path, pathlen, "%s/molko", SHAREDIR);
-		} else if (is_absolute(BINDIR)) {
-			/* SHAREDIR is relative but BINDIR is absolute */
-			snprintf(path, pathlen, "%s/%s/molko", PREFIX, SHAREDIR);
-		} else {
-			/* SHAREDIR, BINDIR are both relative */
-			char *ptr = strstr(base, BINDIR);
-
-			if (ptr) {
-				*ptr = '\0';
-				snprintf(path, pathlen, "%s%s/molko", base, SHAREDIR);
-			} else {
-				/* Unable to determine. */
-				snprintf(path, pathlen, ".");
-			}
-		}
-	}
-
-	SDL_free(base);
+static bool
+is_absolute(const char *path)
+{
+	return path[0] == '/';
 }
 
 #endif
 
+static char *
+normalize(char *str)
+{
+	for (char *p = str; *p; ++p)
+		if (*p == '\\')
+			*p = '/';
+
+	return str;
+}
+
 bool
-sys_init(void)
+sys_init(const char *organization, const char *name)
 {
 #if defined(__MINGW64__)
 	/* On MinGW buffering leads to painful debugging. */
@@ -106,6 +93,9 @@
 	setbuf(stdout, NULL);
 #endif
 
+	snprintf(info.organization, sizeof (info.organization), "%s", organization);
+	snprintf(info.name, sizeof (info.name), "%s", name);
+
 	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0)
 		return errorf("%s", SDL_GetError());
 	if (IMG_Init(IMG_INIT_PNG) != IMG_INIT_PNG)
@@ -122,55 +112,97 @@
 	return true;
 }
 
-const char *
-sys_datadir(void)
+static const char *
+absolute(const char *which)
 {
 	static char path[PATH_MAX];
 
-	if (path[0] == '\0')
-		determine(path, sizeof (path));
+	snprintf(path, sizeof (path), "%s", which);
 
-	return path;
+	return normalize(path);
 }
 
-const char *
-sys_datapath(const char *fmt, ...)
+static const char *
+system_directory(enum sys_dir kind)
 {
-	const char *ret;
-	va_list ap;
+	static char path[PATH_MAX];
+	static char ret[PATH_MAX];
+	char *base, *binsect;
+
+	/* 1. Get current binary directory. */
+	base = SDL_GetBasePath();
+
+	/*
+	 * 2. Decompose the path to the given special directory by computing
+	 *    relative directory to it from where the binary is located.
+	 *
+	 * Example:
+	 *   PREFIX/bin/mlk
+	 *   PREFIX/share/molko
+	 *
+	 * The path to the data is ../share/molko starting from the binary.
+	 *
+	 * If path to binary is absolute we can't determine relative paths to
+	 * any other directory and use the absolute one instead.
+	 *
+	 * Also, on some platforms SDL_GetBasePath isn't implemented and returns
+	 * NULL, in that case return the fallback to the installation prefix.
+	 */
+	if (is_absolute(paths[SYS_DIR_BIN]) || is_absolute(paths[kind]) || !base)
+		return absolute(abspaths[kind]);
 
-	va_start(ap, fmt);
-	ret = sys_datapathv(fmt, ap);
-	va_end(ap);
+	/*
+	 * 3. Put the base path into the path and remove the value of
+	 *    MOLKO_BINDIR.
+	 *
+	 * Example:
+	 *   from: /usr/local/bin
+	 *   to:   /usr/local
+	 */
+	snprintf(path, sizeof (path), "%s", base);
+	SDL_free(base);
+
+	if ((binsect = strstr(path, paths[SYS_DIR_BIN])))
+		*binsect = '\0';
+
+	/* 4. For data directories, we append the program name. */
+	if (kind == SYS_DIR_DATA)
+		snprintf(ret, sizeof (ret), "%s%s/%s", path, paths[kind], info.name);
+	else
+		snprintf(ret, sizeof (ret), "%s%s", path, paths[kind]);
 
-	return ret;
+	return normalize(ret);
+}
+
+static const char *
+user_directory(enum sys_dir kind)
+{
+	/* Kept for future use. */
+	(void)kind;
+
+	static char path[PATH_MAX];
+	char *pref;
+
+	if ((pref = SDL_GetPrefPath(info.organization, info.name))) {
+		snprintf(path, sizeof (path), "%s", pref);
+		SDL_free(pref);
+	} else
+		snprintf(path, sizeof (path), "./");
+
+	return NULL;
 }
 
 const char *
-sys_datapathv(const char *fmt, va_list ap)
+sys_dir(enum sys_dir kind)
 {
-	static char path[PATH_MAX];
-	char filename[FILENAME_MAX];
-
-	vsnprintf(filename, sizeof (filename), fmt, ap);
-	snprintf(path, sizeof (path), "%s/%s", sys_datadir(), filename);
-
-	return path;
-}
-
-const char *
-sys_savepath(unsigned int idx)
-{
-	static char path[PATH_MAX];
-	char *pref;
-
-	if ((pref = SDL_GetPrefPath("malikania", "molko"))) {
-		snprintf(path, sizeof (path), "%ssave-%u", pref, idx);
-		SDL_free(pref);
-	} else
-		snprintf(path, sizeof (path), "save-%u", idx);
-
-	return path;
+	switch (kind) {
+	case SYS_DIR_BIN:
+	case SYS_DIR_DATA:
+	case SYS_DIR_LOCALE:
+		return system_directory(kind);
+	default:
+		return user_directory(kind);
+	}
 }
 
 void
--- a/libcore/core/sys.h	Fri Nov 27 21:34:07 2020 +0100
+++ b/libcore/core/sys.h	Sat Nov 28 18:00:05 2020 +0100
@@ -29,55 +29,39 @@
 #include <stdbool.h>
 
 /**
+ * \brief Kind of special directories.
+ */
+enum sys_dir {
+	SYS_DIR_BIN,            /*!< Path to binaries. */
+	SYS_DIR_DATA,           /*!< Directory containing data. */
+	SYS_DIR_LOCALE,         /*!< Path to NLS catalogs. */
+	SYS_DIR_SAVE,           /*!< User directory for save databases. */
+};
+
+/**
  * Initialize the system.
  *
  * This function is automatically called from \ref core_init and thus not
  * necessary from user.
  *
+ * \pre organization != NULL
+ * \pre name != NULL
+ * \param organization the name of the organization
+ * \param name the game name
  * \return False on error.
  */
 bool
-sys_init(void);
-
-/**
- * Get the base system directory path.
- *
- * \return the path where the executable lives
- */
-const char *
-sys_datadir(void);
-
-/**
- * Construct path to assets directory using printf-like format.
- *
- * \param fmt the format string
- * \return the path to the file
- * \note This function returns pointer to static string.
- */
-const char *
-sys_datapath(const char *fmt, ...);
+sys_init(const char *organization, const char *name);
 
 /**
- * Similar to \a sys_datapath.
+ * Get a system or user directory preferred for this platform.
  *
- * \param fmt the format string
- * \param ap the variadic arguments pointer
- * \return the path to the file
- * \note This function returns pointer to static string.
+ * \pre kind must be valid
+ * \param kind kind of special directory
+ * \return A non-NULL pointer to a static storage path.
  */
 const char *
-sys_datapathv(const char *fmt, va_list ap);
-
-/**
- * Compute the path to the save file for the given game state.
- *
- * \param idx the save number
- * \return the path to the database file
- * \note This only compute the path, it does not check the presence of the file
- * \post The returned value will never be NULL
- */
-const char *
-sys_savepath(unsigned int idx);
+sys_dir(enum sys_dir kind);
 
 /**
  * Close the system.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libcore/core/sysconfig.h.in	Sat Nov 28 18:00:05 2020 +0100
@@ -0,0 +1,32 @@
+/*
+ * sysconfig.h -- build time configuration
+ *
+ * Copyright (c) 2020 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MOLKO_CORE_SYSCONFIG_H
+#define MOLKO_CORE_SYSCONFIG_H
+
+#cmakedefine MOLKO_WITH_NLS
+
+#define MOLKO_BINDIR            "@CMAKE_INSTALL_BINDIR@"
+#define MOLKO_DATADIR           "@CMAKE_INSTALL_DATADIR@"
+#define MOLKO_LOCALEDIR         "@CMAKE_INSTALL_LOCALEDIR@"
+
+#define MOLKO_ABS_BINDIR        "@CMAKE_INSTALL_FULL_BINDIR"
+#define MOLKO_ABS_DATADIR       "@CMAKE_INSTALL_FULL_DATADIR"
+#define MOLKO_ABS_LOCALEDIR     "@CMAKE_INSTALL_FULL_LOCALEDIR"
+
+#endif /* !MOLKO_CORE_SYSCONFIG_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libcore/core/translate.c	Sat Nov 28 18:00:05 2020 +0100
@@ -0,0 +1,44 @@
+/*
+ * translate.c -- native language support
+ *
+ * Copyright (c) 2020 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "sysconfig.h"
+
+#if defined(MOLKO_WITH_NLS)
+#	include <libintl.h>
+#endif
+
+#include "sys.h"
+#include "translate.h"
+#include "util.h"
+
+#include <stdio.h>
+
+bool
+translate_init(const char *name)
+{
+#if defined(MOLKO_WITH_NLS)
+	if (!bindtextdomain(name, sys_dir(SYS_DIR_LOCALE)))
+		return false;
+#endif
+	return true;
+}
+
+void
+translate_finish(void)
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libcore/core/translate.h	Sat Nov 28 18:00:05 2020 +0100
@@ -0,0 +1,40 @@
+/*
+ * translate.h -- native language support
+ *
+ * Copyright (c) 2020 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MOLKO_CORE_TRANSLATE_H
+#define MOLKO_CORE_TRANSLATE_H
+
+#include <stdbool.h>
+
+/**
+ * Initialize native language support.
+ *
+ * \pre name != NULL
+ * \param name the domain name to initialize (e.g mlk-libcore)
+ * \return True if correctly initialized.
+ */
+bool
+translate_init(const char *name);
+
+/**
+ * Close the native language support.
+ */
+void
+translate_finish(void);
+
+#endif /* !MOLKO_CORE_TRANSLATE_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libcore/nls/fr.po	Sat Nov 28 18:00:05 2020 +0100
@@ -0,0 +1,37 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-11-28 16:45+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: /Users/markand/Dev/molko/libcore/core/panic.c:30
+#, c-format
+msgid "abort: %s\n"
+msgstr "fatal: %s\n"
+
+#: /Users/markand/Dev/molko/libcore/core/panic.c:76
+#, c-format
+msgid "abort: panic handler returned\n"
+msgstr "fatal: la fonction de panique n'aurait pas du continuer\n"
+
+#: /Users/markand/Dev/molko/libcore/core/save.c:85
+msgid "database not initialized correctly"
+msgstr "database non initialisée"
+
+#: /Users/markand/Dev/molko/libcore/core/save.c:175
+#, c-format
+msgid "property '%s' was not found"
+msgstr "propriété '%s' non trouvée"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libcore/nls/libcore.pot	Sat Nov 28 18:00:05 2020 +0100
@@ -0,0 +1,37 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-11-28 17:53+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: /Users/markand/Dev/molko/libcore/core/panic.c:30
+#, c-format
+msgid "abort: %s\n"
+msgstr ""
+
+#: /Users/markand/Dev/molko/libcore/core/panic.c:76
+#, c-format
+msgid "abort: panic handler returned\n"
+msgstr ""
+
+#: /Users/markand/Dev/molko/libcore/core/save.c:85
+msgid "database not initialized correctly"
+msgstr ""
+
+#: /Users/markand/Dev/molko/libcore/core/save.c:175
+#, c-format
+msgid "property '%s' was not found"
+msgstr ""
--- a/librpg/CMakeLists.txt	Fri Nov 27 21:34:07 2020 +0100
+++ b/librpg/CMakeLists.txt	Sat Nov 28 18:00:05 2020 +0100
@@ -74,6 +74,7 @@
 	${librpg_SOURCE_DIR}/rpg/message.h
 	${librpg_SOURCE_DIR}/rpg/rpg.c
 	${librpg_SOURCE_DIR}/rpg/rpg.h
+	${librpg_SOURCE_DIR}/rpg/rpg_p.h
 	${librpg_SOURCE_DIR}/rpg/selection.h
 	${librpg_SOURCE_DIR}/rpg/spell.c
 	${librpg_SOURCE_DIR}/rpg/spell.h
@@ -87,9 +88,15 @@
 	${librpg_SOURCE_DIR}/rpg/walksprite.h
 )
 
+set(
+	PO
+	${librpg_SOURCE_DIR}/nls/fr.po
+)
+
 molko_define_library(
 	TARGET librpg
-	SOURCES ${SOURCES}
+	TRANSLATIONS fr
+	SOURCES ${SOURCES} ${PO}
 	LIBRARIES
 		libcore
 		libui
@@ -98,4 +105,4 @@
 )
 
 source_group(rpg FILES ${SOURCES})
-
+source_group(nls FILES ${PO})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/librpg/nls/fr.po	Sat Nov 28 18:00:05 2020 +0100
@@ -0,0 +1,134 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-11-28 17:41+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: /Users/markand/Dev/molko/librpg/rpg/battle-bar.c:127
+msgid "Attack"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/battle-bar.c:134
+msgid "Magic"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/battle-bar.c:141
+msgid "Objects"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/battle-bar.c:148
+msgid "Special"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/battle-state-victory.c:88
+msgid "Victory!"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/battle-state-lost.c:87
+msgid "You have been defeated..."
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/tileset-file.c:241
+msgid "could not parse image"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:130
+msgid "could not parse tileset"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:92
+#, c-format
+msgid "ignoring action %d,%d,%u,%u,%s"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:61
+#, c-format
+msgid "invalid layer type: %s"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:175
+msgid "invalid origin"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/message.c:277
+msgid "message has null dimensions"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/message.c:140
+#, c-format
+msgid "message height too small: %u < %u"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/message.c:171
+msgid "message is automatic but has zero timeout"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/message.c:138
+#, c-format
+msgid "message width too small: %u < %u"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:236
+msgid "missing background layer"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:238
+msgid "missing foreground layer"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:114
+msgid "missing layer type definition"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:110
+msgid "missing map dimensions before layer"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/tileset-file.c:239
+msgid "missing tile dimensions before image"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:240
+msgid "missing tileset"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/tileset-file.c:298
+msgid "missing tileset image"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:229
+msgid "missing title"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:157
+msgid "null map columns"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:166
+msgid "null map rows"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:146
+msgid "null map title"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/tileset-file.c:142
+msgid "tileheight is null"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/tileset-file.c:133
+msgid "tilewidth is null"
+msgstr ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/librpg/nls/librpg.pot	Sat Nov 28 18:00:05 2020 +0100
@@ -0,0 +1,134 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-11-28 17:51+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: /Users/markand/Dev/molko/librpg/rpg/battle-bar.c:127
+msgid "Attack"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/battle-bar.c:134
+msgid "Magic"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/battle-bar.c:141
+msgid "Objects"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/battle-bar.c:148
+msgid "Special"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/battle-state-victory.c:88
+msgid "Victory!"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/battle-state-lost.c:87
+msgid "You have been defeated..."
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/tileset-file.c:241
+msgid "could not parse image"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:130
+msgid "could not parse tileset"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:92
+#, c-format
+msgid "ignoring action %d,%d,%u,%u,%s"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:61
+#, c-format
+msgid "invalid layer type: %s"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:175
+msgid "invalid origin"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/message.c:277
+msgid "message has null dimensions"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/message.c:140
+#, c-format
+msgid "message height too small: %u < %u"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/message.c:171
+msgid "message is automatic but has zero timeout"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/message.c:138
+#, c-format
+msgid "message width too small: %u < %u"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:236
+msgid "missing background layer"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:238
+msgid "missing foreground layer"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:114
+msgid "missing layer type definition"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:110
+msgid "missing map dimensions before layer"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/tileset-file.c:239
+msgid "missing tile dimensions before image"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:240
+msgid "missing tileset"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/tileset-file.c:298
+msgid "missing tileset image"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:229
+msgid "missing title"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:157
+msgid "null map columns"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:166
+msgid "null map rows"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/map-file.c:146
+msgid "null map title"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/tileset-file.c:142
+msgid "tileheight is null"
+msgstr ""
+
+#: /Users/markand/Dev/molko/librpg/rpg/tileset-file.c:133
+msgid "tilewidth is null"
+msgstr ""
--- a/librpg/rpg/battle-bar.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/librpg/rpg/battle-bar.c	Sat Nov 28 18:00:05 2020 +0100
@@ -32,6 +32,7 @@
 #include "battle-bar.h"
 #include "character.h"
 #include "spell.h"
+#include "rpg_p.h"
 
 static void
 draw_status_character_stats(const struct battle *bt,
@@ -115,7 +116,7 @@
 static void
 draw_menu(const struct battle_bar *bar, const struct battle *bt)
 {
-	static struct {
+	struct {
 		unsigned int w, h;
 		enum align align;
 		struct label label;
@@ -123,28 +124,28 @@
 		{
 			.align = ALIGN_TOP,
 			.label = {
-				.text = "Attack",
+				.text = _("Attack"),
 				.flags = LABEL_FLAGS_SHADOW
 			}
 		},
 		{
 			.align = ALIGN_RIGHT,
 			.label = {
-				.text = "Magic",
+				.text = _("Magic"),
 				.flags = LABEL_FLAGS_SHADOW
 			}
 		},
 		{
 			.align = ALIGN_BOTTOM,
 			.label = {
-				.text = "Objects",
+				.text = _("Objects"),
 				.flags = LABEL_FLAGS_SHADOW
 			}
 		},
 		{
 			.align = ALIGN_LEFT,
 			.label = {
-				.text = "Special",
+				.text = _("Special"),
 				.flags = LABEL_FLAGS_SHADOW
 			}
 		}
--- a/librpg/rpg/battle-state-lost.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/librpg/rpg/battle-state-lost.c	Sat Nov 28 18:00:05 2020 +0100
@@ -29,6 +29,7 @@
 #include "battle-state.h"
 #include "battle-state-closing.h"
 #include "battle-state-victory.h"
+#include "rpg_p.h"
 
 struct lost {
 	struct battle_state self;
@@ -83,7 +84,7 @@
 	lost->self.update = update;
 	lost->self.draw = draw;
 
-	lost->msg.text[0] = "You have been defeated...";
+	lost->msg.text[0] = _("You have been defeated...");
 	lost->msg.theme = bt->theme;
 	lost->msg.flags = MESSAGE_FLAGS_AUTOMATIC |
 			 MESSAGE_FLAGS_FADEIN |
--- a/librpg/rpg/battle-state-victory.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/librpg/rpg/battle-state-victory.c	Sat Nov 28 18:00:05 2020 +0100
@@ -29,6 +29,7 @@
 #include "battle-state.h"
 #include "battle-state-closing.h"
 #include "battle-state-victory.h"
+#include "rpg_p.h"
 
 struct victory {
 	struct battle_state self;
@@ -84,7 +85,7 @@
 	vic->self.update = update;
 	vic->self.draw = draw;
 
-	vic->msg.text[0] = "Victory!";
+	vic->msg.text[0] = _("Victory!");
 	vic->msg.theme = bt->theme;
 	vic->msg.flags = MESSAGE_FLAGS_AUTOMATIC |
 	                 MESSAGE_FLAGS_FADEIN |
--- a/librpg/rpg/map-file.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/librpg/rpg/map-file.c	Sat Nov 28 18:00:05 2020 +0100
@@ -32,6 +32,7 @@
 #include <core/trace.h>
 
 #include "map-file.h"
+#include "rpg_p.h"
 
 /* Create %<v>c string literal for scanf */
 #define MAX_F(v) MAX_F_(v)
@@ -57,7 +58,7 @@
 	else if (strcmp(layer_name, "above") == 0)
 		layer_type = MAP_LAYER_TYPE_ABOVE;
 	else
-		return errorf("invalid layer type: %s", layer_name);
+		return errorf(_("invalid layer type: %s"), layer_name);
 
 	amount = ctx->map->columns * ctx->map->rows;
 	current = 0;
@@ -88,7 +89,7 @@
 		struct action *act;
 
 		if (!ctx->mf->load_action) {
-			tracef("ignoring action %d,%d,%u,%u,%s", x, y, w, h, exec);
+			tracef(_("ignoring action %d,%d,%u,%u,%s"), x, y, w, h, exec);
 			continue;
 		}
 
@@ -106,11 +107,11 @@
 
 	/* Check if weight/height has been specified. */
 	if (ctx->map->columns == 0 || ctx->map->rows == 0)
-		return errorf("missing map dimensions before layer");
+		return errorf(_("missing map dimensions before layer"));
 
 	/* Determine layer type. */
 	if (sscanf(line, "layer|%32s", layer_name) <= 0)
-		return errorf("missing layer type definition");
+		return errorf(_("missing layer type definition"));
 
 	if (strcmp(layer_name, "actions") == 0)
 		return parse_actions(ctx);
@@ -126,7 +127,7 @@
 	struct tileset_file *tf = &mf->tileset_file;
 
 	if (!(p = strchr(line, '|')))
-		return errorf("could not parse tileset");
+		return errorf(_("could not parse tileset"));
 
 	snprintf(path, sizeof (path), "%s/%s", ctx->basedir, p + 1);
 
@@ -142,7 +143,7 @@
 parse_title(struct context *ctx, const char *line)
 {
 	if (sscanf(line, "title|" MAX_F(MAP_FILE_TITLE_MAX), ctx->mf->title) != 1 || strlen(ctx->mf->title) == 0)
-		return errorf("null map title");
+		return errorf(_("null map title"));
 
 	ctx->map->title = ctx->mf->title;
 
@@ -153,7 +154,7 @@
 parse_columns(struct context *ctx, const char *line)
 {
 	if (sscanf(line, "columns|%u", &ctx->map->columns) != 1 || ctx->map->columns == 0)
-		return errorf("null map columns");
+		return errorf(_("null map columns"));
 
 	return true;
 }
@@ -162,7 +163,7 @@
 parse_rows(struct context *ctx, const char *line)
 {
 	if (sscanf(line, "rows|%u", &ctx->map->rows) != 1 || ctx->map->rows == 0)
-		return errorf("null map rows");
+		return errorf(_("null map rows"));
 
 	return true;
 }
@@ -171,7 +172,7 @@
 parse_origin(struct context *ctx, const char *line)
 {
 	if (sscanf(line, "origin|%d|%d", &ctx->map->player_x, &ctx->map->player_y) != 2)
-		return errorf("invalid origin");
+		return errorf(_("invalid origin"));
 
 	return true;
 }
@@ -225,18 +226,18 @@
 	 * Check that we have parsed every required components.
 	 */
 	if (!map->title)
-		return errorf("missing title");
+		return errorf(_("missing title"));
 
 	/*
 	 * We don't need to check width/height because parsing layers and
 	 * tilesets already check for their presence, so only check layers.
 	 */
 	if (!map->layers[0].tiles)
-		return errorf("missing background layer");
+		return errorf(_("missing background layer"));
 	if (!map->layers[1].tiles)
-		return errorf("missing foreground layer");
+		return errorf(_("missing foreground layer"));
 	if (!tileset_ok(map->tileset))
-		return errorf("missing tileset");
+		return errorf(_("missing tileset"));
 
 	return true;
 }
--- a/librpg/rpg/message.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/librpg/rpg/message.c	Sat Nov 28 18:00:05 2020 +0100
@@ -36,6 +36,7 @@
 #include <ui/theme.h>
 
 #include "message.h"
+#include "rpg_p.h"
 
 #define THEME(msg)      (msg->theme ? msg->theme : theme_default())
 
@@ -134,9 +135,9 @@
 		label.flags = LABEL_FLAGS_SHADOW;
 
 		if (label.x + lw > msg->w)
-			tracef("message width too small: %u < %u", msg->w, min_width(msg));
+			tracef(_("message width too small: %u < %u"), msg->w, min_width(msg));
 		if (label.y + lh > msg->h)
-			tracef("message height too small: %u < %u", msg->h, min_height(msg));
+			tracef(_("message height too small: %u < %u"), msg->h, min_height(msg));
 
 		/*
 		 * The function label_draw will use THEME_COLOR_NORMAL to draw
@@ -167,7 +168,7 @@
 	    : MESSAGE_STATE_SHOWING;
 
 	if (msg->flags & MESSAGE_FLAGS_AUTOMATIC && msg->timeout == 0)
-		tracef("message is automatic but has zero timeout");
+		tracef(_("message is automatic but has zero timeout"));
 }
 
 void
@@ -273,7 +274,7 @@
 	unsigned int w, h;
 
 	if (msg->w == 0 || msg->h == 0) {
-		tracef("message has null dimensions");
+		tracef(_("message has null dimensions"));
 		return;
 	}
 
--- a/librpg/rpg/rpg.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/librpg/rpg/rpg.c	Sat Nov 28 18:00:05 2020 +0100
@@ -16,18 +16,21 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <core/translate.h>
+
 #include "rpg.h"
+#include "rpg_p.h"
 
 bool
 rpg_init(void)
 {
-	/* Currently empty, placeholder for future. */
+#if defined(MOLKO_WITH_NLS)
+	translate_init("mlk-librpg");
+#endif
+
 	return true;
 }
 
-/**
- * Close the rpg library.
- */
 void
 rpg_finish(void)
 {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/librpg/rpg/rpg_p.h	Sat Nov 28 18:00:05 2020 +0100
@@ -0,0 +1,31 @@
+/*
+ * rpg_p -- librpg private definitions
+ *
+ * Copyright (c) 2020 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MOLKO_RPG_RPG_P_H
+#define MOLKO_RPG_RPG_P_H
+
+#include "sysconfig.h"
+
+#if defined(MOLKO_WITH_NLS)
+#       include <libintl.h>
+#       define _(s) dgettext("mlk-librpg", s)
+#else
+#       define _(s)
+#endif
+
+#endif /* !MOLKO_RPG_RPG_P_H */
--- a/librpg/rpg/tileset-file.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/librpg/rpg/tileset-file.c	Sat Nov 28 18:00:05 2020 +0100
@@ -32,6 +32,7 @@
 #include <core/image.h>
 #include <core/util.h>
 
+#include "rpg_p.h"
 #include "tileset-file.h"
 #include "tileset.h"
 
@@ -129,7 +130,7 @@
 parse_tilewidth(struct context *ctx, const char *line)
 {
 	if (sscanf(line, "tilewidth|%u", &ctx->tilewidth) != 1 || ctx->tilewidth == 0)
-		return errorf("tilewidth is null");
+		return errorf(_("tilewidth is null"));
 
 	return true;
 }
@@ -138,7 +139,7 @@
 parse_tileheight(struct context *ctx, const char *line)
 {
 	if (sscanf(line, "tileheight|%u", &ctx->tileheight) != 1 || ctx->tileheight == 0)
-		return errorf("tileheight is null");
+		return errorf(_("tileheight is null"));
 
 	return true;
 }
@@ -235,9 +236,9 @@
 	char *p;
 
 	if (ctx->tilewidth == 0 || ctx->tileheight == 0)
-		return errorf("missing tile dimensions before image");
+		return errorf(_("missing tile dimensions before image"));
 	if (!(p = strchr(line, '|')))
-		return errorf("could not parse image");
+		return errorf(_("could not parse image"));
 
 	if (!image_open(&ctx->tf->image, pprintf("%s/%s", ctx->basedir, p + 1)))
 		return false;
@@ -294,7 +295,7 @@
 check(const struct tileset *tileset)
 {
 	if (!tileset->sprite)
-		return errorf("missing tileset image");
+		return errorf(_("missing tileset image"));
 
 	return true;
 }
--- a/libui/CMakeLists.txt	Fri Nov 27 21:34:07 2020 +0100
+++ b/libui/CMakeLists.txt	Sat Nov 28 18:00:05 2020 +0100
@@ -25,6 +25,11 @@
 )
 
 set(
+	PO
+	${libui_SOURCE_DIR}/nls/fr.po
+)
+
+set(
 	SOURCES
 	${libui_SOURCE_DIR}/ui/align.c
 	${libui_SOURCE_DIR}/ui/align.h
@@ -44,11 +49,13 @@
 	${libui_SOURCE_DIR}/ui/theme.h
 	${libui_SOURCE_DIR}/ui/ui.c
 	${libui_SOURCE_DIR}/ui/ui.h
+	${libui_SOURCE_DIR}/ui/ui_p.h
 )
 
 molko_define_library(
 	TARGET libui
-	SOURCES ${SOURCES}
+	TRANSLATIONS fr
+	SOURCES ${SOURCES} ${PO} ${ASSETS}
 	ASSETS ${ASSETS}
 	LIBRARIES
 		libcore
@@ -56,4 +63,6 @@
 		$<BUILD_INTERFACE:${libui_SOURCE_DIR}>
 )
 
+source_group(TREE ${libui_SOURCE_DIR}/ui/assets PREFIX assets FILES ${ASSETS})
 source_group(ui FILES ${SOURCES})
+source_group(nls FILES ${PO})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libui/nls/fr.po	Sat Nov 28 18:00:05 2020 +0100
@@ -0,0 +1,38 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-11-28 17:33+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: /Users/markand/Dev/molko/libui/ui/button.c:75
+#, c-format
+msgid "button height is too small for text: %u < %u"
+msgstr ""
+
+#: /Users/markand/Dev/molko/libui/ui/button.c:73
+#, c-format
+msgid "button width is too small for text: %u < %u"
+msgstr ""
+
+#: /Users/markand/Dev/molko/libui/ui/gridmenu.c:86
+#, c-format
+msgid "gridmenu height is too small: %u < %u"
+msgstr ""
+
+#: /Users/markand/Dev/molko/libui/ui/gridmenu.c:78
+#, c-format
+msgid "gridmenu width is too small: %u < %u"
+msgstr ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libui/nls/libui.pot	Sat Nov 28 18:00:05 2020 +0100
@@ -0,0 +1,38 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-11-28 17:50+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: /Users/markand/Dev/molko/libui/ui/button.c:75
+#, c-format
+msgid "button height is too small for text: %u < %u"
+msgstr ""
+
+#: /Users/markand/Dev/molko/libui/ui/button.c:73
+#, c-format
+msgid "button width is too small for text: %u < %u"
+msgstr ""
+
+#: /Users/markand/Dev/molko/libui/ui/gridmenu.c:86
+#, c-format
+msgid "gridmenu height is too small: %u < %u"
+msgstr ""
+
+#: /Users/markand/Dev/molko/libui/ui/gridmenu.c:78
+#, c-format
+msgid "gridmenu width is too small: %u < %u"
+msgstr ""
--- a/libui/ui/button.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/libui/ui/button.c	Sat Nov 28 18:00:05 2020 +0100
@@ -29,6 +29,7 @@
 #include "button.h"
 #include "label.h"
 #include "theme.h"
+#include "ui_p.h"
 
 static bool
 is_boxed(const struct button *button, const struct event_click *click)
@@ -69,9 +70,9 @@
 	label_query(&label, &lw, &lh);
 
 	if (lw > button->w)
-		tracef("button is too small for text: %u < %u", button->w, lw);
+		tracef(_("button width is too small for text: %u < %u"), button->w, lw);
 	if (lh > button->h)
-		tracef("button is too small for text: %u < %u", button->h, lh);
+		tracef(_("button height is too small for text: %u < %u"), button->h, lh);
 
 	align(ALIGN_CENTER, &label.x, &label.y, lw, lh,
 	    button->x, button->y, button->w, button->h);
--- a/libui/ui/gridmenu.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/libui/ui/gridmenu.c	Sat Nov 28 18:00:05 2020 +0100
@@ -26,6 +26,7 @@
 #include <core/texture.h>
 #include <core/trace.h>
 
+#include "ui_p.h"
 #include "frame.h"
 #include "label.h"
 #include "gridmenu.h"
@@ -74,7 +75,7 @@
 
 	/* Compute spacing between elements. */
 	if (hreq > menu->w) {
-		tracef("gridmenu width is too small: %u < %u", menu->w, vreq);
+		tracef(_("gridmenu width is too small: %u < %u"), menu->w, vreq);
 		gtex->spaceh = 1;
 	} else {
 		hreq -= theme->padding * 2;
@@ -82,7 +83,7 @@
 	}
 
 	if (vreq > menu->h) {
-		tracef("gridmenu height is too small: %u < %u", menu->h, vreq);
+		tracef(_("gridmenu height is too small: %u < %u"), menu->h, vreq);
 		gtex->spacev = 1;
 	} else {
 		vreq -= theme->padding * 2;
--- a/libui/ui/ui.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/libui/ui/ui.c	Sat Nov 28 18:00:05 2020 +0100
@@ -16,12 +16,19 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <core/translate.h>
+
+#include "ui_p.h"
 #include "ui.h"
 #include "theme.h"
 
 bool
 ui_init(void)
 {
+#if defined(MOLKO_WITH_NLS)
+	translate_init("mlk-libui");
+#endif
+
 	return theme_init();
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libui/ui/ui_p.h	Sat Nov 28 18:00:05 2020 +0100
@@ -0,0 +1,31 @@
+/*
+ * ui_p -- libui private definitions
+ *
+ * Copyright (c) 2020 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MOLKO_UI_UI_P_H
+#define MOLKO_UI_UI_P_H
+
+#include "sysconfig.h"
+
+#if defined(MOLKO_WITH_NLS)
+#       include <libintl.h>
+#       define _(s) dgettext("mlk-libui", s)
+#else
+#       define _(s)
+#endif
+
+#endif /* !MOLKO_UI_UI_P_H */
--- a/tests/test-map.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/tests/test-map.c	Sat Nov 28 18:00:05 2020 +0100
@@ -159,7 +159,7 @@
 	 * we will skip if it fails to initialize.
 	 */
 
-	if (core_init() && window_open("test-map", 100, 100)) {
+	if (core_init("fr.malikania", "test") && window_open("test-map", 100, 100)) {
 		GREATEST_RUN_SUITE(suite_basics);
 		GREATEST_RUN_SUITE(suite_errors);
 	}
--- a/tests/test-tileset.c	Fri Nov 27 21:34:07 2020 +0100
+++ b/tests/test-tileset.c	Sat Nov 28 18:00:05 2020 +0100
@@ -115,7 +115,7 @@
 {
 	GREATEST_MAIN_BEGIN();
 
-	if (core_init() && window_open("test-tileset", 100, 100)) {
+	if (core_init("fr.malikania", "test") && window_open("test-tileset", 100, 100)) {
 		GREATEST_RUN_SUITE(suite_basics);
 		GREATEST_RUN_SUITE(suite_errors);
 	}