aboutsummaryrefslogtreecommitdiff
path: root/core/src/Utils/I18n.cpp
blob: 645cfc523f612286b864e7ab83f597c89548a24a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
#include "I18n.hpp"

#include "Utils/String.hpp"

#include <json/reader.h>
#include <json/value.h>
#include <tsl/array_map.h>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string_view>
#include <utility>

namespace fs = std::filesystem;
namespace {

struct LanguageInfo {
	std::string codeName;
	std::string localeName;
	fs::path file;
};

class I18nState {
public:
	static I18nState& Get() {
		static I18nState instance;
		return instance;
	}

public:
	tsl::array_map<char, LanguageInfo> localeInfos;
	tsl::array_map<char, std::string> currentEntries;
	LanguageInfo* currentLanguage;
};

std::string findLocalizedName(const fs::path& localeFile) {
	std::ifstream ifs{ localeFile };
	if (!ifs) {
		// TODO log error
		throw std::runtime_error("Failed to open locale file.");
	}

	Json::Value root;
	ifs >> root;
	if (auto& name = root["$localized_name"]; name.isString()) {
		return std::string(name.asCString());
	} else {
		// TODO log error
		throw std::runtime_error("Failed to find $localized_name in language file.");
	}
}

} // namespace

void I18n::Init() {
	auto& state = I18nState::Get();

	auto dir = fs::current_path() / "locale";
	if (!fs::exists(dir)) {
		// TODO log error
		return;
	}

	for (auto& elm : fs::directory_iterator{ dir }) {
		if (!elm.is_regular_file()) continue;

		auto& path = elm.path();
		auto codeName = path.stem().string();

		state.localeInfos.emplace(
			codeName,
			LanguageInfo{
				.codeName = codeName,
				.localeName = findLocalizedName(path),
				.file = path,
			});
	}

	SetLanguage("en_US");
}

void I18n::Shutdown() {
	// Nothing to do yet
}

Signal<> I18n::reloadSignal{};

void I18n::ReloadLocales() {
	auto& state = I18nState::Get();
	reloadSignal();
}

std::string_view I18n::GetLanguage() {
	auto& state = I18nState::Get();
	return state.currentLanguage->localeName;
}

bool I18n::SetLanguage(std::string_view lang) {
	auto& state = I18nState::Get();
	if (!state.currentLanguage) return false;
	if (state.currentLanguage->codeName == lang) return false;

	if (auto iter = state.localeInfos.find(lang); iter != state.localeInfos.end()) {
		state.currentLanguage = &iter.value();
		state.currentEntries.clear();

		auto& file = state.currentLanguage->file;
		std::ifstream ifs{ file };
		Json::Value root;
		ifs >> root;

		for (auto name : root.getMemberNames()) {
			auto& value = root[name];
		}
	}
	ReloadLocales();
	return true;
}

std::optional<std::string_view> I18n::Lookup(std::string_view key) {
	auto& state = I18nState::Get();
	auto iter = state.currentEntries.find(key);
	if (iter != state.currentEntries.end()) {
		return iter.value();
	} else {
		return std::nullopt;
	}
}

std::string_view I18n::LookupUnwrap(std::string_view key) {
	auto o = Lookup(key);
	if (!o) {
		std::string msg;
		msg.append("Unable to find locale for '");
		msg.append(key);
		msg.append("'.");
		throw std::runtime_error(std::move(msg));
	};
	return o.value();
}

BasicTranslation::BasicTranslation(std::string_view key)
	: mContent{ I18n::LookupUnwrap(key) } {
}

std::string_view BasicTranslation::Get() const {
	return mContent;
}

FormattedTranslation::FormattedTranslation(std::string_view key) {
	auto src = I18n::LookupUnwrap(key);

	mMinimumResultLen = 0;

	bool escape = false;
	bool matchingCloseBrace = false;
	std::string buf;
	for (char c : src) {
		switch (c) {
			case '\\': {
				// Disallow double (or more) escaping
				if (escape) throw std::runtime_error("Cannot escape '\\'.");

				escape = true;
				continue;
			}

			case '{': {
				// Escaping an opeing brace cause the whole "argument" (if any) gets parsed as a part of the previous literal
				if (escape) {
					buf += '{';
					break;
				}

				// Generate literal
				mMinimumResultLen += buf.size();
				mParsedElements.push_back(Element{ std::move(buf) }); // Should also clear buf

				matchingCloseBrace = true;
			} break;
			case '}': {
				if (escape) throw std::runtime_error("Cannot escape '}', put \\ before the '{' if intended to escape braces.");

				// If there is no pairing '{', simply treat this as a normal character
				// (escaping for closing braces)
				if (!matchingCloseBrace) {
					buf += '}';
					break;
				}

				// Generate argument
				if (buf.empty()) {
					// No index given, default to use current argument's index
					auto currArgIdx = (int)mNumArguments;
					mParsedElements.push_back(Element{ currArgIdx });
				} else {
					// Use provided index
					int argIdx = std::stoi(buf);
					mParsedElements.push_back(Element{ argIdx });
					buf.clear();
				}
			} break;

			default: {
				if (escape) throw std::runtime_error("Cannot escape normal character '" + std::to_string(c) + "'.");

				buf += c;
			} break;
		}

		escape = false;
	}
}

std::string FormattedTranslation::Format(std::span<Argument> args) {
	if (args.size() != mNumArguments) {
		throw std::runtime_error("Invalid number of arguments for FormattedTranslation::Format, expected " + std::to_string(mNumArguments) + " but found " + std::to_string(args.size()) + ".");
	}

	std::string result;
	result.reserve(mMinimumResultLen);

	for (auto& elm : mParsedElements) {
		if (auto literal = std::get_if<std::string>(&elm)) {
			result.append(*literal);
		}
		if (auto idx = std::get_if<int>(&elm)) {
			result.append(args[*idx]);
		}
	}

	return result;
}