aboutsummaryrefslogtreecommitdiff
path: root/core/text/i18n/i18n.odin
blob: 148fe229f4865ed70fb9353b3dcade8b927782ed (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
package i18n
/*
	Internationalization helpers.

	Copyright 2021-2022 Jeroen van Rijn <nom@duclavier.com>.
	Made available under Odin's license.

	List of contributors:
		Jeroen van Rijn: Initial implementation.
*/
import    "core:strings"

// Currently active catalog.
ACTIVE: ^Translation

// Allow between 1 and 255 plural forms. Default: 10.
MAX_PLURALS :: min(max(#config(ODIN_i18N_MAX_PLURAL_FORMS, 10), 1), 255)

// The main data structure. This can be generated from various different file formats, as long as we have a parser for them.
Section :: map[string][]string

Translation :: struct {
	k_v:    map[string]Section, // k_v[section][key][plural_form] = ...
	intern: strings.Intern,

	pluralize: proc(number: int) -> int,
}

Error :: enum {
	// General return values.
	None = 0,
	Empty_Translation_Catalog,
	Duplicate_Key,

	// Couldn't find, open or read file.
	File_Error,

	// File too short.
	Premature_EOF,

	// GNU Gettext *.MO file errors.
	MO_File_Invalid_Signature,
	MO_File_Unsupported_Version,
	MO_File_Invalid,
	MO_File_Incorrect_Plural_Count,

	// Qt Linguist *.TS file errors.
	TS_File_Parse_Error,
	TS_File_Expected_Context,
	TS_File_Expected_Context_Name,
	TS_File_Expected_Source,
	TS_File_Expected_Translation,
	TS_File_Expected_NumerusForm,
	Bad_Str,
	Bad_Id,

}

Parse_Options :: struct {
	merge_sections: bool,
}

DEFAULT_PARSE_OPTIONS :: Parse_Options{
	merge_sections = false,
}

/*
	Returns the first translation string for the passed `key`.
	It is also aliased with `get()`.

	Two ways to use it:
	- get(key), which defaults to the `i18n.ACTIVE` catalogue, or
	- get(key, catalog) to grab text from a specific loaded catalogue

	Inputs:
	- key:     the string to translate
	- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)

	Returns:   the translated string, or the original `key` if no translation was found.
*/
get_single_section :: proc(key: string, catalog: ^Translation = ACTIVE) -> (value: string) {
	return get_by_slot(key, 0, catalog)
}

/*
	Returns the first translation string for the passed `key` in a specific section or context.
	It is also aliases with `get()`.

	Two ways to use it:
	- get(section, key), which defaults to the `i18n.ACTIVE` catalogue, or
	- get(section, key, catalog) to grab text from a specific loaded catalogue

	Inputs:
	- section: the catalogue section (sometimes also called 'context') in which to look up the translation
	- key:     the string to translate
	- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)

	Returns:   the translated string, or the original `key` if no translation was found.
*/
get_by_section :: proc(section, key: string, catalog: ^Translation = ACTIVE) -> (value: string) {
	return get_by_slot(section, key, 0, catalog)
}

get :: proc{get_single_section, get_by_section}

/*
	Returns the translation string for the passed `key` in a specific plural form (if present in the catalogue).
	It is also aliased with `get_n()`.

	Two ways to use it:
	- get_n(key, quantity), which returns the appropriate plural from the active catalogue, or
	- get_n(key, quantity, catalog) to grab text from a specific loaded catalogue

	Inputs:
	- key:      the string to translate
	- quantity: the quantity of item to be used to select the correct plural form
	- catalog:  the catalogue to use for the translation (defaults to i18n.ACTIVE)

	Returns:    the translated string, or the original `key` if no translation was found.
*/
get_single_section_with_quantity :: proc(key: string, quantity: int, catalog: ^Translation = ACTIVE) -> (value: string) {
	/*
		A lot of languages use singular for 1 item and plural for 0 or more than 1 items. This is our default pluralize rule.
	*/
	slot := 1 if quantity != 1 else 0

	if catalog.pluralize != nil {
		slot = catalog.pluralize(quantity)
	}
	return get_by_slot(key, slot, catalog)
}

/*
	Returns the translation string for the passed `key` in a specific plural form (if present in the catalogue)
	in a specific section or context.
	It is also aliases with `get_n()`.

	Two ways to use it:
	- get(section, key, quantity), which returns the appropriate plural from the active catalogue, or
	- get(section, key, quantity, catalog) to grab text from a specific loaded catalogue

	Inputs:
	- section: the catalogue section (sometime also called 'context') from which to lookup the translation
	- key:     the string to translate
	- qantity: the quantity of item to be used to select the correct plural form
	- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)

	Returns:   the translated string, or the original `key` if no translation was found
*/
get_by_section_with_quantity :: proc(section, key: string, quantity: int, catalog: ^Translation = ACTIVE) -> (value: string) {
	/*
		A lot of languages use singular for 1 item and plural for 0 or more than 1 items. This is our default pluralize rule.
	*/
	slot := 1 if quantity != 1 else 0

	if catalog.pluralize != nil {
		slot = catalog.pluralize(quantity)
	}
	return get_by_slot(section, key, slot, catalog)
}
get_n :: proc{get_single_section_with_quantity, get_by_section_with_quantity}

/*
	Two ways to use:
	- get_by_slot(key, slot), which returns the requested plural from the active catalogue, or
	- get_by_slot(key, slot, catalog) to grab text from a specific loaded catalogue.

	If a file format parser doesn't (yet) support plural slots, each of the slots will point at the same string.
	- section: the catalogue section (sometime also called 'context') from which to lookup the translation

	Inputs:
	- key:     the string to translate.
	- slot:    the translation slot to choose (slots refer to plural forms specific for each language and their meaning changes from catalogue to catalogue).
	- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)

	Returns:   the translated string, or the original `key` if no translation was found.
*/
get_by_slot_single_section :: proc(key: string, slot: int, catalog: ^Translation = ACTIVE) -> (value: string) {
	return get_by_slot_by_section("", key, slot, catalog)
}

/*
	Two ways to use:
	- get_by_slot(key, slot), which returns the requested plural from the active catalog, or
	- get_by_slot(key, slot, catalog) to grab text from a specific one.

	If a file format parser doesn't (yet) support plural slots, each of the slots will point at the same string.

	Inputs:
	- section: the catalogue section (sometime also called 'context') from which to lookup the translation
	- key:     the string to translate.
	- slot:    the translation slot to choose (slots refer to plural forms specific for each language and their meaning changes from catalogue to catalogue).
	- catalog: the catalogue to use for the translation (defaults to i18n.ACTIVE)

	Returns:   the translated string or the original `key` if no translation was found.
*/
get_by_slot_by_section :: proc(section, key: string, slot: int, catalog: ^Translation = ACTIVE) -> (value: string) {
	if catalog == nil || section not_in catalog.k_v {
		// Return the key if the catalog catalog hasn't been initialized yet, or the section is not present.
		return key
	}

	// Return the translation from the requested slot if this key is known, else return the key.
	if translations, ok := catalog.k_v[section][key]; ok {
		plural := min(max(0, slot), len(catalog.k_v[section][key]) - 1)
		return translations[plural]
	}
	return key
}
get_by_slot :: proc{get_by_slot_single_section, get_by_slot_by_section}

/*
	- destroy(), to clean up the currently active catalog catalog i18n.ACTIVE
	- destroy(catalog), to clean up a specific catalog.
*/
destroy :: proc(catalog: ^Translation = ACTIVE, allocator := context.allocator) {
	context.allocator = allocator

	if catalog == nil {
		return
	}

	for section in catalog.k_v {
		for key in catalog.k_v[section] {
			delete(catalog.k_v[section][key])
		}
		delete(catalog.k_v[section])
	}
	delete(catalog.k_v)
	strings.intern_destroy(&catalog.intern)
	free(catalog)
}