about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--data/languages/swedish.txt211
-rw-r--r--scripts/update_localization.py92
-rw-r--r--src/base/system.c26
-rw-r--r--src/base/system.h35
-rw-r--r--src/game/client/components/menus.cpp145
-rw-r--r--src/game/client/components/menus.hpp2
-rw-r--r--src/game/client/components/menus_settings.cpp82
-rw-r--r--src/game/client/gameclient.hpp1
-rw-r--r--src/game/localization.cpp56
-rw-r--r--src/game/localization.hpp46
10 files changed, 623 insertions, 73 deletions
diff --git a/data/languages/swedish.txt b/data/languages/swedish.txt
new file mode 100644
index 00000000..705f02b0
--- /dev/null
+++ b/data/languages/swedish.txt
@@ -0,0 +1,211 @@
+
+##### translated strings #####
+
+Fullscreen
+== Fullskärm
+
+Are you sure that you want to quit?
+== Är du säker på att du vill avsluta?
+
+Loading
+== Laddar
+
+Mouse sens.
+== Muskänslighet
+
+Reset to defaults
+== Återställ till standard
+
+Mute when not active
+== 
+
+Controls
+== Kontroller
+
+Current
+== Nuvarande
+
+Sound volume
+== Ljud volym
+
+Dynamic Camera
+== Dynamisk kamera
+
+Enter
+== Fortsätt
+
+Internet
+== Internet
+
+Quit
+== Avsluta
+
+Use sounds
+== Använd ljudeffekter
+
+Nickname:
+== Smeknamn:
+
+Movement
+== Förflyttning
+
+Body
+== Kropp
+
+Hue
+== Nyans
+
+UI Color
+== Gränssnittfärg
+
+Demos
+== Demon
+
+Texture Compression
+== Texturkompression
+
+Show only supported
+== Visa endast upplösningar som stöds
+
+No
+== Nej
+
+Connecting to
+== Ansluter till
+
+Miscellaneous
+== Övrigt
+
+Switch weapon on pickup
+== Byt vapen vid upplock
+
+Show name plates
+== Visa namnskyltar
+
+Quality Textures
+== Kvalitets texturer
+
+Game
+== Spel
+
+Favorites
+== Favoriter
+
+Graphics
+== Grafik
+
+Alpha
+== Genomskinlighet
+
+Name:
+== Namn:
+
+Weapon
+== Vapen
+
+Sound
+== Ljud
+
+The server is running a non-standard tuning on a pure game mode.
+== 
+
+Ok
+== Ok
+
+Call Vote
+== 
+
+Settings
+== Inställningar
+
+You must restart the game for all settings to take effect.
+== Du måste starta om spelet för att ändringarna skall gälla.
+
+Custom colors
+== Egna färger
+
+Try again
+== Försök igen
+
+Disconnected
+== 
+
+Display Modes
+== 
+
+FSAA samples
+== 
+
+Downloading map
+== Laddar ner karta
+
+Welcome to Teeworlds
+== Välkommen till Teeworlds
+
+News
+== Nyheter
+
+Lht.
+== Ljusstyrka
+
+Voting
+== Röstning
+
+Sat.
+== Mättnad
+
+Password Incorrect
+== Felaktigt lösenord
+
+LAN
+== LAN
+
+Sample rate
+== 
+
+Password:
+== Lösenord:
+
+As this is the first time you launch the game, please enter your nick name below. It's recommended that you check the settings to adjust them to your liking before joining a server.
+== Detta är första gången du startar spelet, var vänligen skriv in ditt smeknamn här nedanför. Det är rekommenderat att du kollar inställningarna och justerar dom till din preferens innan du börjar spela.
+
+V-Sync
+== V-Sync
+
+Skins
+== Utseende
+
+Feet
+== Fötter
+
+Player
+== Spelare
+
+Abort
+== Avbryt
+
+Always show name plates
+== Visa alltid namnskyltar
+
+Chat
+== Chat
+
+Server Info
+== Server info.
+
+Yes
+== Ja
+
+High Detail
+== Extra detaljer
+
+##### needs translation ####
+
+##### old translations ####
+
+Try Again
+== Försök igen
+
+sasdf
+== asdfsa
+
diff --git a/scripts/update_localization.py b/scripts/update_localization.py
new file mode 100644
index 00000000..7dbc90f5
--- /dev/null
+++ b/scripts/update_localization.py
@@ -0,0 +1,92 @@
+import sys, os
+
+source_exts = [".c", ".cpp", ".h", ".hpp"]
+
+def parse_source():
+	stringtable = {}
+	def process_line(line):
+		if 'localize("' in line:
+			fields = line.split('localize("', 2)[1].split('"', 2)
+			stringtable[fields[0]] = ""
+			process_line(fields[1])
+
+	for root, dirs, files in os.walk("src"):
+		for name in files:
+			filename = os.path.join(root, name)
+			
+			if os.sep + "external" + os.sep in filename:
+				continue
+			
+			if filename[-2:] in source_exts or filename[-4:] in source_exts:
+				for line in file(filename):
+					process_line(line)
+	
+	return stringtable
+
+def load_languagefile(filename):
+	f = file(filename)
+	lines = f.readlines()
+	f.close()
+	stringtable = {}
+
+	for i in xrange(0, len(lines)-1):
+		l = lines[i].strip()
+		if len(l) and not l[0] == '=' and not l[0] == '#':
+			stringtable[l] = lines[i+1][3:].strip()
+			
+	return stringtable
+
+
+def generate_languagefile(outputfilename, srctable, loctable):
+	f = file(outputfilename, "w")
+
+	num_items = 0
+	new_items = 0
+	old_items = 0
+
+	print >>f, ""
+	print >>f, "##### translated strings #####"
+	print >>f, ""
+	for k in srctable:
+		if k in loctable:
+			print >>f, k
+			print >>f, "==", loctable[k]
+			print >>f, ""
+			num_items += 1
+
+
+	print >>f, "##### needs translation ####"
+	print >>f,  ""
+	for k in srctable:
+		if not k in loctable:
+			print >>f, k
+			print >>f, "==", srctable[k]
+			print >>f, ""
+			num_items += 1
+			new_items += 1
+
+	print >>f, "##### old translations ####"
+	print >>f, ""
+	for k in loctable:
+		if not k in srctable:
+			print >>f, k
+			print >>f, "==", loctable[k]
+			print >>f, ""
+			num_items += 1
+			old_items += 1
+
+	print "%-40s %8s %8s %8s" % ("filename", "total", "new", "old")
+	print "%-40s %8d %8d %8d" % (outputfilename, num_items, new_items, old_items)
+	f.close()
+
+srctable = parse_source()
+
+for filename in os.listdir("data/languages"):
+	if not ".txt" in filename:
+		continue
+		
+	filename = "data/languages/" + filename
+	generate_languagefile(filename, srctable, load_languagefile(filename))
+
+
+
diff --git a/src/base/system.c b/src/base/system.c
index c94e4cf9..067c870f 100644
--- a/src/base/system.c
+++ b/src/base/system.c
@@ -1097,6 +1097,12 @@ int str_comp_nocase(const char *a, const char *b)
 #endif
 }
 
+int str_comp(const char *a, const char *b)
+{
+	return strcmp(a, b);
+}
+
+
 const char *str_find_nocase(const char *haystack, const char *needle)
 {
 	while(*haystack) /* native implementation */
@@ -1116,6 +1122,26 @@ const char *str_find_nocase(const char *haystack, const char *needle)
 	return 0;
 }
 
+
+const char *str_find(const char *haystack, const char *needle)
+{
+	while(*haystack) /* native implementation */
+	{
+		const char *a = haystack;
+		const char *b = needle;
+		while(*a && *b && *a == *b)
+		{
+			a++;
+			b++;
+		}
+		if(!(*b))
+			return haystack;
+		haystack++;
+	}
+	
+	return 0;
+}
+
 void str_hex(char *dst, int dst_size, const void *data, int data_size)
 {
 	static const char hex[] = "0123456789ABCDEF";
diff --git a/src/base/system.h b/src/base/system.h
index 87cae4a0..6ead7239 100644
--- a/src/base/system.h
+++ b/src/base/system.h
@@ -763,6 +763,25 @@ void str_sanitize(char *str);
 */
 int str_comp_nocase(const char *a, const char *b);
 
+
+/*
+	Function: str_comp_nocase
+		Compares to strings case sensitive.
+	
+	Parameters:
+		a - String to compare.
+		b - String to compare.
+	
+	Returns:	
+		<0 - String a is lesser then string b
+		0 - String a is equal to string b
+		>0 - String a is greater then string b
+
+	Remarks:
+		- The strings are treated as zero-termineted strings.
+*/
+int str_comp(const char *a, const char *b);
+
 /*
 	Function: str_find_nocase
 		Finds a string inside another string case insensitive.
@@ -781,6 +800,22 @@ int str_comp_nocase(const char *a, const char *b);
 */
 const char *str_find_nocase(const char *haystack, const char *needle);
 
+/*
+	Function: str_find
+		Finds a string inside another string case sensitive.
+
+	Parameters:
+		haystack - String to search in
+		needle - String to search for
+		
+	Returns:
+		A pointer into haystack where the needle was found.
+		Returns NULL of needle could not be found.
+
+	Remarks:
+		- The strings are treated as zero-termineted strings.
+*/
+const char *str_find(const char *haystack, const char *needle);
 
 /*
 	Function: str_hex
diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp
index 0fd024c3..398bd1ae 100644
--- a/src/game/client/components/menus.cpp
+++ b/src/game/client/components/menus.cpp
@@ -12,13 +12,16 @@
 #include "skins.hpp"
 
 #include <engine/e_client_interface.h>
-
+extern "C" {
+#include <engine/e_linereader.h>
+}
 #include <game/version.hpp>
 #include <game/generated/g_protocol.hpp>
 
 #include <game/generated/gc_data.hpp>
 #include <game/client/gameclient.hpp>
 #include <game/client/lineinput.hpp>
+#include <game/localization.hpp>
 #include <mastersrv/mastersrv.h>
 
 vec4 MENUS::gui_color;
@@ -460,7 +463,7 @@ int MENUS::render_menubar(RECT r)
 		{
 			ui_vsplit_l(&box, 90.0f, &button, &box);
 			static int news_button=0;
-			if (ui_do_button(&news_button, "News", active_page==PAGE_NEWS, &button, ui_draw_menu_tab_button, 0))
+			if (ui_do_button(&news_button, localize("News"), active_page==PAGE_NEWS, &button, ui_draw_menu_tab_button, 0))
 				new_page = PAGE_NEWS;
 			ui_vsplit_l(&box, 30.0f, 0, &box); 
 		}
@@ -468,7 +471,7 @@ int MENUS::render_menubar(RECT r)
 		ui_vsplit_l(&box, 100.0f, &button, &box);
 		static int internet_button=0;
 		int corners = CORNER_TL;
-		if (ui_do_button(&internet_button, "Internet", active_page==PAGE_INTERNET, &button, ui_draw_menu_tab_button, &corners))
+		if (ui_do_button(&internet_button, localize("Internet"), active_page==PAGE_INTERNET, &button, ui_draw_menu_tab_button, &corners))
 		{
 			client_serverbrowse_refresh(BROWSETYPE_INTERNET);
 			new_page = PAGE_INTERNET;
@@ -478,7 +481,7 @@ int MENUS::render_menubar(RECT r)
 		ui_vsplit_l(&box, 80.0f, &button, &box);
 		static int lan_button=0;
 		corners = 0;
-		if (ui_do_button(&lan_button, "LAN", active_page==PAGE_LAN, &button, ui_draw_menu_tab_button, &corners))
+		if (ui_do_button(&lan_button, localize("LAN"), active_page==PAGE_LAN, &button, ui_draw_menu_tab_button, &corners))
 		{
 			client_serverbrowse_refresh(BROWSETYPE_LAN);
 			new_page = PAGE_LAN;
@@ -488,7 +491,7 @@ int MENUS::render_menubar(RECT r)
 		ui_vsplit_l(&box, 110.0f, &button, &box);
 		static int favorites_button=0;
 		corners = CORNER_TR;
-		if (ui_do_button(&favorites_button, "Favorites", active_page==PAGE_FAVORITES, &button, ui_draw_menu_tab_button, &corners))
+		if (ui_do_button(&favorites_button, localize("Favorites"), active_page==PAGE_FAVORITES, &button, ui_draw_menu_tab_button, &corners))
 		{
 			client_serverbrowse_refresh(BROWSETYPE_FAVORITES);
 			new_page  = PAGE_FAVORITES;
@@ -497,7 +500,7 @@ int MENUS::render_menubar(RECT r)
 		ui_vsplit_l(&box, 4.0f*5, 0, &box);
 		ui_vsplit_l(&box, 100.0f, &button, &box);
 		static int demos_button=0;
-		if (ui_do_button(&demos_button, "Demos", active_page==PAGE_DEMOS, &button, ui_draw_menu_tab_button, 0))
+		if (ui_do_button(&demos_button, localize("Demos"), active_page==PAGE_DEMOS, &button, ui_draw_menu_tab_button, 0))
 		{
 			demolist_populate();
 			new_page  = PAGE_DEMOS;
@@ -508,19 +511,19 @@ int MENUS::render_menubar(RECT r)
 		/* online menus */
 		ui_vsplit_l(&box, 90.0f, &button, &box);
 		static int game_button=0;
-		if (ui_do_button(&game_button, "Game", active_page==PAGE_GAME, &button, ui_draw_menu_tab_button, 0))
+		if (ui_do_button(&game_button, localize("Game"), active_page==PAGE_GAME, &button, ui_draw_menu_tab_button, 0))
 			new_page = PAGE_GAME;
 
 		ui_vsplit_l(&box, 4.0f, 0, &box);
 		ui_vsplit_l(&box, 140.0f, &button, &box);
 		static int server_info_button=0;
-		if (ui_do_button(&server_info_button, "Server Info", active_page==PAGE_SERVER_INFO, &button, ui_draw_menu_tab_button, 0))
+		if (ui_do_button(&server_info_button, localize("Server Info"), active_page==PAGE_SERVER_INFO, &button, ui_draw_menu_tab_button, 0))
 			new_page = PAGE_SERVER_INFO;
 
 		ui_vsplit_l(&box, 4.0f, 0, &box);
 		ui_vsplit_l(&box, 140.0f, &button, &box);
 		static int callvote_button=0;
-		if (ui_do_button(&callvote_button, "Call Vote", active_page==PAGE_CALLVOTE, &button, ui_draw_menu_tab_button, 0))
+		if (ui_do_button(&callvote_button, localize("Call Vote"), active_page==PAGE_CALLVOTE, &button, ui_draw_menu_tab_button, 0))
 			new_page = PAGE_CALLVOTE;
 			
 		ui_vsplit_l(&box, 30.0f, 0, &box);
@@ -537,13 +540,13 @@ int MENUS::render_menubar(RECT r)
 	
 	ui_vsplit_r(&box, 90.0f, &box, &button);
 	static int quit_button=0;
-	if (ui_do_button(&quit_button, "Quit", 0, &button, ui_draw_menu_tab_button, 0))
+	if (ui_do_button(&quit_button, localize("Quit"), 0, &button, ui_draw_menu_tab_button, 0))
 		popup = POPUP_QUIT;
 
 	ui_vsplit_r(&box, 10.0f, &box, &button);
 	ui_vsplit_r(&box, 120.0f, &box, &button);
 	static int settings_button=0;
-	if (ui_do_button(&settings_button, "Settings", active_page==PAGE_SETTINGS, &button, ui_draw_menu_tab_button, 0))
+	if (ui_do_button(&settings_button, localize("Settings"), active_page==PAGE_SETTINGS, &button, ui_draw_menu_tab_button, 0))
 		new_page = PAGE_SETTINGS;
 	
 	if(new_page != -1)
@@ -593,7 +596,7 @@ void MENUS::render_loading(float percent)
 	gfx_quads_end();
 
 
-	const char *caption = "Loading";
+	const char *caption = localize("Loading");
 
 	tw = gfx_text_width(0, 48.0f, caption, -1);
 	RECT r;
@@ -617,8 +620,77 @@ void MENUS::render_news(RECT main_view)
 	ui_draw_rect(&main_view, color_tabbar_active, CORNER_ALL, 10.0f);
 }
 
-void MENUS::init()
+void MENUS::on_init()
 {
+	LINEREADER lr;
+	IOHANDLE io = io_open("swedish.txt", IOFLAG_READ);
+	linereader_init(&lr, io);
+	char *line;
+	while((line = linereader_get(&lr)))
+	{
+		if(!str_length(line))
+			continue;
+			
+		char *replacement = linereader_get(&lr);
+		if(!replacement)
+		{
+			dbg_msg("", "unexpected end of file");
+			break;
+		}
+		
+		if(replacement[0] != '=' || replacement[1] != '=' || replacement[2] != ' ')
+		{
+			dbg_msg("", "malform replacement line for '%s'", line);
+			continue;
+		}
+
+		replacement += 3;
+		localization.add_string(line, replacement);
+	}
+	
+	/*
+	array<string> my_strings;
+	array<string>::range r2;
+	my_strings.add("4");
+	my_strings.add("6");
+	my_strings.add("1");
+	my_strings.add("3");
+	my_strings.add("7");
+	my_strings.add("5");
+	my_strings.add("2");
+
+	for(array<string>::range r = my_strings.all(); !r.empty(); r.pop_front())
+		dbg_msg("", "%s", r.front().cstr());
+		
+	sort(my_strings.all());
+	
+	dbg_msg("", "after:");
+	for(array<string>::range r = my_strings.all(); !r.empty(); r.pop_front())
+		dbg_msg("", "%s", r.front().cstr());
+		
+	
+	array<int> myarray;
+	myarray.add(4);
+	myarray.add(6);
+	myarray.add(1);
+	myarray.add(3);
+	myarray.add(7);
+	myarray.add(5);
+	myarray.add(2);
+
+	for(array<int>::range r = myarray.all(); !r.empty(); r.pop_front())
+		dbg_msg("", "%d", r.front());
+		
+	sort(myarray.all());
+	sort_verify(myarray.all());
+	
+	dbg_msg("", "after:");
+	for(array<int>::range r = myarray.all(); !r.empty(); r.pop_front())
+		dbg_msg("", "%d", r.front());
+	
+	exit(-1);
+	// */
+	
 	if(config.cl_show_welcome)
 		popup = POPUP_FIRST_LAUNCH;
 	config.cl_show_welcome = 0;
@@ -709,49 +781,46 @@ int MENUS::render()
 		
 		if(popup == POPUP_CONNECTING)
 		{
-			title = "Connecting to";
+			title = localize("Connecting to");
 			extra_text = config.ui_server_address;  // TODO: query the client about the address
-			button_text = "Abort";
+			button_text = localize("Abort");
 			if(client_mapdownload_totalsize() > 0)
 			{
-				title = "Downloading map";
+				title = localize("Downloading map");
 				str_format(buf, sizeof(buf), "%d/%d KiB", client_mapdownload_amount()/1024, client_mapdownload_totalsize()/1024);
 				extra_text = buf;
 			}
 		}
 		else if(popup == POPUP_DISCONNECTED)
 		{
-			title = "Disconnected";
+			title = localize("Disconnected");
 			extra_text = client_error_string();
-			button_text = "Ok";
+			button_text = localize("Ok");
 			extra_align = -1;
 		}
 		else if(popup == POPUP_PURE)
 		{
-			title = "Disconnected";
-			extra_text = "The server is running a non-standard tuning on a pure game mode.";
-			button_text = "Ok";
+			title = localize("Disconnected");
+			extra_text = localize("The server is running a non-standard tuning on a pure game mode.");
+			button_text = localize("Ok");
 			extra_align = -1;
 		}
 		else if(popup == POPUP_PASSWORD)
 		{
-			title = "Password Error";
+			title = localize("Password Incorrect");
 			extra_text = client_error_string();
-			button_text = "Try Again";
+			button_text = localize("Try again");
 		}
 		else if(popup == POPUP_QUIT)
 		{
-			title = "Quit";
-			extra_text = "Are you sure that you want to quit?";
+			title = localize("Quit");
+			extra_text = localize("Are you sure that you want to quit?");
 		}
 		else if(popup == POPUP_FIRST_LAUNCH)
 		{
-			title = "Welcome to Teeworlds";
-			extra_text =
-			"As this is the first time you launch the game, please enter your nick name below. "
-			"It's recommended that you check the settings to adjust them to your liking "
-			"before joining a server.";
-			button_text = "Ok";
+			title = localize("Welcome to Teeworlds");
+			extra_text = localize("As this is the first time you launch the game, please enter your nick name below. It's recommended that you check the settings to adjust them to your liking before joining a server.");
+			button_text = localize("Ok");
 			extra_align = -1;
 		}
 		
@@ -788,11 +857,11 @@ int MENUS::render()
 			ui_vmargin(&no, 20.0f, &no);
 
 			static int button_abort = 0;
-			if(ui_do_button(&button_abort, "No", 0, &no, ui_draw_menu_button, 0) || escape_pressed)
+			if(ui_do_button(&button_abort, localize("No"), 0, &no, ui_draw_menu_button, 0) || escape_pressed)
 				popup = POPUP_NONE;
 
 			static int button_tryagain = 0;
-			if(ui_do_button(&button_tryagain, "Yes", 0, &yes, ui_draw_menu_button, 0) || enter_pressed)
+			if(ui_do_button(&button_tryagain, localize("Yes"), 0, &yes, ui_draw_menu_button, 0) || enter_pressed)
 				client_quit();
 		}
 		else if(popup == POPUP_PASSWORD)
@@ -809,11 +878,11 @@ int MENUS::render()
 			ui_vmargin(&abort, 20.0f, &abort);
 			
 			static int button_abort = 0;
-			if(ui_do_button(&button_abort, "Abort", 0, &abort, ui_draw_menu_button, 0) || escape_pressed)
+			if(ui_do_button(&button_abort, localize("Abort"), 0, &abort, ui_draw_menu_button, 0) || escape_pressed)
 				popup = POPUP_NONE;
 
 			static int button_tryagain = 0;
-			if(ui_do_button(&button_tryagain, "Try again", 0, &tryagain, ui_draw_menu_button, 0) || enter_pressed)
+			if(ui_do_button(&button_tryagain, localize("Try again"), 0, &tryagain, ui_draw_menu_button, 0) || enter_pressed)
 			{
 				client_connect(config.ui_server_address);
 			}
@@ -825,7 +894,7 @@ int MENUS::render()
 			ui_vsplit_l(&label, 100.0f, 0, &textbox);
 			ui_vsplit_l(&textbox, 20.0f, 0, &textbox);
 			ui_vsplit_r(&textbox, 60.0f, &textbox, 0);
-			ui_do_label(&label, "Password:", 20, -1);
+			ui_do_label(&label, localize("Password:"), 20, -1);
 			ui_do_edit_box(&config.password, &textbox, config.password, sizeof(config.password), 14.0f, true);
 		}
 		else if(popup == POPUP_FIRST_LAUNCH)
@@ -837,7 +906,7 @@ int MENUS::render()
 			ui_vmargin(&part, 80.0f, &part);
 			
 			static int enter_button = 0;
-			if(ui_do_button(&enter_button, "Enter", 0, &part, ui_draw_menu_button, 0) || enter_pressed)
+			if(ui_do_button(&enter_button, localize("Enter"), 0, &part, ui_draw_menu_button, 0) || enter_pressed)
 				popup = POPUP_NONE;
 			
 			ui_hsplit_b(&box, 40.f, &box, &part);
@@ -847,7 +916,7 @@ int MENUS::render()
 			ui_vsplit_l(&label, 100.0f, 0, &textbox);
 			ui_vsplit_l(&textbox, 20.0f, 0, &textbox);
 			ui_vsplit_r(&textbox, 60.0f, &textbox, 0);
-			ui_do_label(&label, "Nickname:", 20, -1);
+			ui_do_label(&label, localize("Nickname:"), 20, -1);
 			ui_do_edit_box(&config.player_name, &textbox, config.player_name, sizeof(config.player_name), 14.0f);
 		}
 		else
diff --git a/src/game/client/components/menus.hpp b/src/game/client/components/menus.hpp
index 27423490..6d304628 100644
--- a/src/game/client/components/menus.hpp
+++ b/src/game/client/components/menus.hpp
@@ -173,7 +173,7 @@ public:
 
 	bool is_active() const { return menu_active; }
 
-	void init();
+	virtual void on_init();
 
 	virtual void on_statechange(int new_state, int old_state);
 	virtual void on_reset();
diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp
index b240c607..beeab046 100644
--- a/src/game/client/components/menus_settings.cpp
+++ b/src/game/client/components/menus_settings.cpp
@@ -54,7 +54,7 @@ void MENUS::render_settings_player(RECT main_view)
 	// render settings
 	{	
 		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-		ui_do_label(&button, "Name:", 14.0, -1);
+		ui_do_label(&button, localize("Name:"), 14.0, -1);
 		ui_vsplit_l(&button, 80.0f, 0, &button);
 		ui_vsplit_l(&button, 180.0f, &button, 0);
 		if(ui_do_edit_box(config.player_name, &button, config.player_name, sizeof(config.player_name), 14.0f))
@@ -62,7 +62,7 @@ void MENUS::render_settings_player(RECT main_view)
 
 		static int dynamic_camera_button = 0;
 		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-		if(ui_do_button(&dynamic_camera_button, "Dynamic Camera", config.cl_mouse_deadzone != 0, &button, ui_draw_checkbox, 0))
+		if(ui_do_button(&dynamic_camera_button, localize("Dynamic Camera"), config.cl_mouse_deadzone != 0, &button, ui_draw_checkbox, 0))
 		{
 			
 			if(config.cl_mouse_deadzone)
@@ -80,25 +80,25 @@ void MENUS::render_settings_player(RECT main_view)
 		}
 
 		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-		if (ui_do_button(&config.cl_autoswitch_weapons, "Switch weapon on pickup", config.cl_autoswitch_weapons, &button, ui_draw_checkbox, 0))
+		if (ui_do_button(&config.cl_autoswitch_weapons, localize("Switch weapon on pickup"), config.cl_autoswitch_weapons, &button, ui_draw_checkbox, 0))
 			config.cl_autoswitch_weapons ^= 1;
 			
 		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-		if (ui_do_button(&config.cl_nameplates, "Show name plates", config.cl_nameplates, &button, ui_draw_checkbox, 0))
+		if (ui_do_button(&config.cl_nameplates, localize("Show name plates"), config.cl_nameplates, &button, ui_draw_checkbox, 0))
 			config.cl_nameplates ^= 1;
 
 		//if(config.cl_nameplates)
 		{
 			ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
 			ui_vsplit_l(&button, 15.0f, 0, &button);
-			if (ui_do_button(&config.cl_nameplates_always, "Always show name plates", config.cl_nameplates_always, &button, ui_draw_checkbox, 0))
+			if (ui_do_button(&config.cl_nameplates_always, localize("Always show name plates"), config.cl_nameplates_always, &button, ui_draw_checkbox, 0))
 				config.cl_nameplates_always ^= 1;
 		}
 			
 		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
 		
 		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-		if (ui_do_button(&config.player_color_body, "Custom colors", config.player_use_custom_color, &button, ui_draw_checkbox, 0))
+		if (ui_do_button(&config.player_color_body, localize("Custom colors"), config.player_use_custom_color, &button, ui_draw_checkbox, 0))
 		{
 			config.player_use_custom_color = config.player_use_custom_color?0:1;
 			need_sendinfo = true;
@@ -110,8 +110,13 @@ void MENUS::render_settings_player(RECT main_view)
 			colors[0] = &config.player_color_body;
 			colors[1] = &config.player_color_feet;
 			
-			const char *parts[] = {"Body", "Feet"};
-			const char *labels[] = {"Hue", "Sat.", "Lht."};
+			const char *parts[] = {
+				localize("Body"),
+				localize("Feet")};
+			const char *labels[] = {
+				localize("Hue"),
+				localize("Sat."),
+				localize("Lht.")};
 			static int color_slider[2][3] = {{0}};
 			//static float v[2][3] = {{0, 0.5f, 0.25f}, {0, 0.5f, 0.25f}};
 				
@@ -129,7 +134,7 @@ void MENUS::render_settings_player(RECT main_view)
 					RECT text;
 					ui_hsplit_t(&main_view, 19.0f, &button, &main_view);
 					ui_vsplit_l(&button, 30.0f, 0, &button);
-					ui_vsplit_l(&button, 30.0f, &text, &button);
+					ui_vsplit_l(&button, 70.0f, &text, &button);
 					ui_vsplit_r(&button, 5.0f, &button, 0);
 					ui_hsplit_t(&button, 4.0f, 0, &button);
 					
@@ -154,7 +159,7 @@ void MENUS::render_settings_player(RECT main_view)
 	RECT header, footer;
 	ui_hsplit_t(&skinselection, 20, &header, &skinselection);
 	ui_draw_rect(&header, vec4(1,1,1,0.25f), CORNER_T, 5.0f); 
-	ui_do_label(&header, "Skins", 18.0f, 0);
+	ui_do_label(&header, localize("Skins"), 18.0f, 0);
 
 	// draw footers	
 	ui_hsplit_b(&skinselection, 20, &skinselection, &footer);
@@ -248,6 +253,7 @@ typedef struct
 	int keyid;
 } KEYINFO;
 
+// TODO: localize
 KEYINFO keys[] = 
 {
 	{ "Move Left:", "+left", 0},
@@ -325,7 +331,7 @@ void MENUS::render_settings_controls(RECT main_view)
 		ui_draw_rect(&movement_settings, vec4(1,1,1,0.25f), CORNER_ALL, 10.0f);
 		ui_margin(&movement_settings, 10.0f, &movement_settings);
 		
-		gfx_text(0, movement_settings.x, movement_settings.y, 14, "Movement", -1);
+		gfx_text(0, movement_settings.x, movement_settings.y, 14, localize("Movement"), -1);
 		
 		ui_hsplit_t(&movement_settings, 14.0f+5.0f+10.0f, 0, &movement_settings);
 		
@@ -333,7 +339,7 @@ void MENUS::render_settings_controls(RECT main_view)
 			RECT button, label;
 			ui_hsplit_t(&movement_settings, 20.0f, &button, &movement_settings);
 			ui_vsplit_l(&button, 130.0f, &label, &button);
-			ui_do_label(&label, "Mouse sens.", 14.0f, -1);
+			ui_do_label(&label, localize("Mouse sens."), 14.0f, -1);
 			ui_hmargin(&button, 2.0f, &button);
 			config.inp_mousesens = (int)(ui_do_scrollbar_h(&config.inp_mousesens, &button, (config.inp_mousesens-5)/500.0f)*500.0f)+5;
 			//*key.key = ui_do_key_reader(key.key, &button, *key.key);
@@ -350,7 +356,7 @@ void MENUS::render_settings_controls(RECT main_view)
 		ui_draw_rect(&weapon_settings, vec4(1,1,1,0.25f), CORNER_ALL, 10.0f);
 		ui_margin(&weapon_settings, 10.0f, &weapon_settings);
 
-		gfx_text(0, weapon_settings.x, weapon_settings.y, 14, "Weapon", -1);
+		gfx_text(0, weapon_settings.x, weapon_settings.y, 14, localize("Weapon"), -1);
 		
 		ui_hsplit_t(&weapon_settings, 14.0f+5.0f+10.0f, 0, &weapon_settings);
 		ui_do_getbuttons(5, 12, weapon_settings);
@@ -363,7 +369,7 @@ void MENUS::render_settings_controls(RECT main_view)
 		ui_draw_rect(&voting_settings, vec4(1,1,1,0.25f), CORNER_ALL, 10.0f);
 		ui_margin(&voting_settings, 10.0f, &voting_settings);
 	
-		gfx_text(0, voting_settings.x, voting_settings.y, 14, "Voting", -1);
+		gfx_text(0, voting_settings.x, voting_settings.y, 14, localize("Voting"), -1);
 		
 		ui_hsplit_t(&voting_settings, 14.0f+5.0f+10.0f, 0, &voting_settings);
 		ui_do_getbuttons(12, 14, voting_settings);
@@ -376,7 +382,7 @@ void MENUS::render_settings_controls(RECT main_view)
 		ui_draw_rect(&chat_settings, vec4(1,1,1,0.25f), CORNER_ALL, 10.0f);
 		ui_margin(&chat_settings, 10.0f, &chat_settings);
 	
-		gfx_text(0, chat_settings.x, chat_settings.y, 14, "Chat", -1);
+		gfx_text(0, chat_settings.x, chat_settings.y, 14, localize("Chat"), -1);
 		
 		ui_hsplit_t(&chat_settings, 14.0f+5.0f+10.0f, 0, &chat_settings);
 		ui_do_getbuttons(14, 16, chat_settings);
@@ -389,7 +395,7 @@ void MENUS::render_settings_controls(RECT main_view)
 		ui_draw_rect(&misc_settings, vec4(1,1,1,0.25f), CORNER_ALL, 10.0f);
 		ui_margin(&misc_settings, 10.0f, &misc_settings);
 	
-		gfx_text(0, misc_settings.x, misc_settings.y, 14, "Miscellaneous", -1);
+		gfx_text(0, misc_settings.x, misc_settings.y, 14, localize("Miscellaneous"), -1);
 		
 		ui_hsplit_t(&misc_settings, 14.0f+5.0f+10.0f, 0, &misc_settings);
 		ui_do_getbuttons(16, 21, misc_settings);
@@ -398,7 +404,7 @@ void MENUS::render_settings_controls(RECT main_view)
 	// defaults
 	ui_hsplit_t(&reset_button, 10.0f, 0, &reset_button);
 	static int default_button = 0;
-	if (ui_do_button((void*)&default_button, "Reset to defaults", 0, &reset_button, ui_draw_menu_button, 0))
+	if (ui_do_button((void*)&default_button, localize("Reset to defaults"), 0, &reset_button, ui_draw_menu_button, 0))
 		gameclient.binds->set_defaults();
 }
 
@@ -420,7 +426,7 @@ void MENUS::render_settings_graphics(RECT main_view)
 	// draw allmodes switch
 	RECT header, footer;
 	ui_hsplit_t(&modelist, 20, &button, &modelist);
-	if (ui_do_button(&config.gfx_display_all_modes, "Show only supported", config.gfx_display_all_modes^1, &button, ui_draw_checkbox, 0))
+	if (ui_do_button(&config.gfx_display_all_modes, localize("Show only supported"), config.gfx_display_all_modes^1, &button, ui_draw_checkbox, 0))
 	{
 		config.gfx_display_all_modes ^= 1;
 		num_modes = gfx_get_video_modes(modes, MAX_RESOLUTIONS);
@@ -429,11 +435,11 @@ void MENUS::render_settings_graphics(RECT main_view)
 	// draw header
 	ui_hsplit_t(&modelist, 20, &header, &modelist);
 	ui_draw_rect(&header, vec4(1,1,1,0.25f), CORNER_T, 5.0f); 
-	ui_do_label(&header, "Display Modes", 14.0f, 0);
+	ui_do_label(&header, localize("Display Modes"), 14.0f, 0);
 
 	// draw footers	
 	ui_hsplit_b(&modelist, 20, &modelist, &footer);
-	str_format(buf, sizeof(buf), "Current: %dx%d %d bit", config.gfx_screen_width, config.gfx_screen_height, config.gfx_color_depth);
+	str_format(buf, sizeof(buf), "%s: %dx%d %d bit", localize("Current"), config.gfx_screen_width, config.gfx_screen_height, config.gfx_color_depth);
 	ui_draw_rect(&footer, vec4(1,1,1,0.25f), CORNER_B, 5.0f); 
 	ui_vsplit_l(&footer, 10.0f, 0, &footer);
 	ui_do_label(&footer, buf, 14.0f, -1);
@@ -489,21 +495,21 @@ void MENUS::render_settings_graphics(RECT main_view)
 	
 	// switches
 	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-	if (ui_do_button(&config.gfx_fullscreen, "Fullscreen", config.gfx_fullscreen, &button, ui_draw_checkbox, 0))
+	if (ui_do_button(&config.gfx_fullscreen, localize("Fullscreen"), config.gfx_fullscreen, &button, ui_draw_checkbox, 0))
 	{
 		config.gfx_fullscreen ^= 1;
 		need_restart = true;
 	}
 
 	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-	if (ui_do_button(&config.gfx_vsync, "V-Sync", config.gfx_vsync, &button, ui_draw_checkbox, 0))
+	if (ui_do_button(&config.gfx_vsync, localize("V-Sync"), config.gfx_vsync, &button, ui_draw_checkbox, 0))
 	{
 		config.gfx_vsync ^= 1;
 		need_restart = true;
 	}
 
 	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-	if (ui_do_button(&config.gfx_fsaa_samples, "FSAA samples", config.gfx_fsaa_samples, &button, ui_draw_checkbox_number, 0))
+	if (ui_do_button(&config.gfx_fsaa_samples, localize("FSAA samples"), config.gfx_fsaa_samples, &button, ui_draw_checkbox_number, 0))
 	{
 		config.gfx_fsaa_samples = (config.gfx_fsaa_samples+1)%17;
 		need_restart = true;
@@ -511,21 +517,21 @@ void MENUS::render_settings_graphics(RECT main_view)
 		
 	ui_hsplit_t(&main_view, 40.0f, &button, &main_view);
 	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-	if (ui_do_button(&config.gfx_texture_quality, "Quality Textures", config.gfx_texture_quality, &button, ui_draw_checkbox, 0))
+	if (ui_do_button(&config.gfx_texture_quality, localize("Quality Textures"), config.gfx_texture_quality, &button, ui_draw_checkbox, 0))
 	{
 		config.gfx_texture_quality ^= 1;
 		need_restart = true;
 	}
 
 	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-	if (ui_do_button(&config.gfx_texture_compression, "Texture Compression", config.gfx_texture_compression, &button, ui_draw_checkbox, 0))
+	if (ui_do_button(&config.gfx_texture_compression, localize("Texture Compression"), config.gfx_texture_compression, &button, ui_draw_checkbox, 0))
 	{
 		config.gfx_texture_compression ^= 1;
 		need_restart = true;
 	}
 
 	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-	if (ui_do_button(&config.gfx_high_detail, "High Detail", config.gfx_high_detail, &button, ui_draw_checkbox, 0))
+	if (ui_do_button(&config.gfx_high_detail, localize("High Detail"), config.gfx_high_detail, &button, ui_draw_checkbox, 0))
 		config.gfx_high_detail ^= 1;
 
 	//
@@ -534,9 +540,13 @@ void MENUS::render_settings_graphics(RECT main_view)
 	ui_hsplit_t(&main_view, 20.0f, 0, &main_view);
 	ui_hsplit_t(&main_view, 20.0f, &text, &main_view);
 	//ui_vsplit_l(&text, 15.0f, 0, &text);
-	ui_do_label(&text, "UI Color", 14.0f, -1);
+	ui_do_label(&text, localize("UI Color"), 14.0f, -1);
 	
-	const char *labels[] = {"Hue", "Sat.", "Lht.", "Alpha"};
+	const char *labels[] = {
+		localize("Hue"),
+		localize("Sat."),
+		localize("Lht."),
+		localize("Alpha")};
 	int *color_slider[4] = {&config.ui_color_hue, &config.ui_color_sat, &config.ui_color_lht, &config.ui_color_alpha};
 	for(int s = 0; s < 4; s++)
 	{
@@ -560,7 +570,7 @@ void MENUS::render_settings_sound(RECT main_view)
 	ui_vsplit_l(&main_view, 300.0f, &main_view, 0);
 	
 	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-	if (ui_do_button(&config.snd_enable, "Use Sounds", config.snd_enable, &button, ui_draw_checkbox, 0))
+	if (ui_do_button(&config.snd_enable, localize("Use sounds"), config.snd_enable, &button, ui_draw_checkbox, 0))
 	{
 		config.snd_enable ^= 1;
 		need_restart = true;
@@ -570,7 +580,7 @@ void MENUS::render_settings_sound(RECT main_view)
 		return;
 	
 	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-	if (ui_do_button(&config.snd_nonactive_mute, "Mute when not active", config.snd_nonactive_mute, &button, ui_draw_checkbox, 0))
+	if (ui_do_button(&config.snd_nonactive_mute, localize("Mute when not active"), config.snd_nonactive_mute, &button, ui_draw_checkbox, 0))
 		config.snd_nonactive_mute ^= 1;
 		
 	// sample rate box
@@ -578,7 +588,7 @@ void MENUS::render_settings_sound(RECT main_view)
 		char buf[64];
 		str_format(buf, sizeof(buf), "%d", config.snd_rate);
 		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
-		ui_do_label(&button, "Sample Rate", 14.0f, -1);
+		ui_do_label(&button, localize("Sample rate"), 14.0f, -1);
 		ui_vsplit_l(&button, 110.0f, 0, &button);
 		ui_vsplit_l(&button, 180.0f, &button, 0);
 		ui_do_edit_box(&config.snd_rate, &button, buf, sizeof(buf), 14.0f);
@@ -599,7 +609,7 @@ void MENUS::render_settings_sound(RECT main_view)
 		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
 		ui_vsplit_l(&button, 110.0f, &label, &button);
 		ui_hmargin(&button, 2.0f, &button);
-		ui_do_label(&label, "Sound Volume", 14.0f, -1);
+		ui_do_label(&label, localize("Sound volume"), 14.0f, -1);
 		config.snd_volume = (int)(ui_do_scrollbar_h(&config.snd_volume, &button, config.snd_volume/100.0f)*100.0f);
 		ui_hsplit_t(&main_view, 20.0f, 0, &main_view);
 	}
@@ -636,7 +646,11 @@ void MENUS::render_settings(RECT main_view)
 	
 	RECT button;
 	
-	const char *tabs[] = {"Player", "Controls", "Graphics", "Sound"};
+	const char *tabs[] = {
+		localize("Player"),
+		localize("Controls"),
+		localize("Graphics"),
+		localize("Sound")};
 	int num_tabs = (int)(sizeof(tabs)/sizeof(*tabs));
 
 	for(int i = 0; i < num_tabs; i++)
@@ -662,6 +676,6 @@ void MENUS::render_settings(RECT main_view)
 	{
 		RECT restart_warning;
 		ui_hsplit_b(&main_view, 40, &main_view, &restart_warning);
-		ui_do_label(&restart_warning, "You must restart the game for all settings to take effect.", 15.0f, -1, 220);
+		ui_do_label(&restart_warning, localize("You must restart the game for all settings to take effect."), 15.0f, -1, 220);
 	}
 }
diff --git a/src/game/client/gameclient.hpp b/src/game/client/gameclient.hpp
index 041df10d..35b95f27 100644
--- a/src/game/client/gameclient.hpp
+++ b/src/game/client/gameclient.hpp
@@ -157,3 +157,4 @@ public:
 
 extern GAMECLIENT gameclient;
 
+extern const char *localize(const char *str);
diff --git a/src/game/localization.cpp b/src/game/localization.cpp
new file mode 100644
index 00000000..cfc659f5
--- /dev/null
+++ b/src/game/localization.cpp
@@ -0,0 +1,56 @@
+
+#include "localization.hpp"
+
+static unsigned str_hash(const char *str)
+{
+	unsigned hash = 5381;
+	for(; *str; str++)
+		hash = ((hash << 5) + hash) + (*str); /* hash * 33 + c */
+	return hash;
+}
+
+const char *localize(const char *str)
+{
+	const char *new_str = localization.find_string(str_hash(str));
+	//dbg_msg("", "no localization for '%s'", str);
+	return new_str ? new_str : str;
+}
+
+LOC_CONSTSTRING::LOC_CONSTSTRING(const char *str)
+{
+	default_str = str;
+	hash = str_hash(default_str);
+	version = -1;
+}
+
+void LOC_CONSTSTRING::reload()
+{
+	version = localization.version();
+	const char *new_str = localization.find_string(hash);
+	current_str = new_str;
+	if(!current_str)
+		current_str = default_str;
+}
+
+LOCALIZATIONDATABASE::LOCALIZATIONDATABASE()
+{
+	current_version = 0;
+}
+
+void LOCALIZATIONDATABASE::add_string(const char *org_str, const char *new_str)
+{
+	STRING s;
+	s.hash = str_hash(org_str);
+	s.replacement = new_str;
+	strings.add(s);
+	
+	current_version++;
+}
+
+const char *LOCALIZATIONDATABASE::find_string(unsigned hash)
+{
+	array<STRING>::range r = ::find(strings.all(), hash);
+	if(r.empty())
+		return 0;
+	return r.front().replacement;
+}
diff --git a/src/game/localization.hpp b/src/game/localization.hpp
new file mode 100644
index 00000000..87b6e2f8
--- /dev/null
+++ b/src/game/localization.hpp
@@ -0,0 +1,46 @@
+#include <base/tl/array.hpp>
+
+class LOCALIZATIONDATABASE
+{
+	class STRING
+	{
+	public:
+		unsigned hash;
+		string replacement;
+		
+		bool operator ==(unsigned h) const { return hash == h; }
+		
+	};
+
+	array<STRING> strings;
+	int current_version;
+	
+public:
+	LOCALIZATIONDATABASE();
+
+	int version() { return current_version; }
+	
+	void add_string(const char *org_str, const char *new_str);
+	const char *find_string(unsigned hash);
+};
+
+static LOCALIZATIONDATABASE localization;
+
+
+class LOC_CONSTSTRING
+{
+	const char *default_str;
+	const char *current_str;
+	unsigned hash;
+	int version;
+public:
+	LOC_CONSTSTRING(const char *str);
+	void reload();
+	
+	inline operator const char *()
+	{
+		if(version != localization.version())
+			reload();
+		return current_str;
+	}
+};