-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtranslator-io.R
More file actions
300 lines (278 loc) · 10.2 KB
/
translator-io.R
File metadata and controls
300 lines (278 loc) · 10.2 KB
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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
#' Read and Write Translations
#'
#' Export [`Translator`][Translator] objects to text files and import such
#' files back into \R as [`Translator`][Translator] objects.
#'
#' @details
#' The information contained within a [`Translator`][Translator] object is
#' split: translations are reorganized by language and exported independently
#' from other fields.
#'
#' [translator_write()] creates two types of file: a single *Translator file*,
#' and zero, or more *translations files*. These are plain text files that can
#' be inspected and modified using a wide variety of tools and systems. They
#' target different audiences:
#'
#' * the Translator file is useful to developers, and
#' * translations files are meant to be shared with non-technical
#' collaborators such as translators.
#'
#' [translator_read()] first reads a Translator file and creates a
#' [`Translator`][Translator] object from it. It then calls
#' [translations_paths()] to list expected translations files (that should
#' normally be stored alongside the Translator file), attempts to read them,
#' and registers successfully imported translations.
#'
#' There are two requirements.
#'
#' * All files must be stored in the same directory. By default, this is set
#' equal to `inst/transltr/` (see `getOption("transltr.path")`).
#' * Filenames of translations files are standardized and must correspond to
#' languages (language codes, see `lang`).
#'
#' The inner workings of the serialization process are thoroughly described in
#' [serialize()].
#'
#' ## Translator file
#'
#' A Translator file contains a [YAML](https://yaml.org/spec/1.1/) (1.1)
#' representation of a [`Translator`][Translator] object stripped of all
#' its translations except those that are registered as source text.
#'
#' ## Translations files
#'
#' A translations file contains a [FLAT][flat_serialize()] representation of
#' a set of translations sharing the same target language. This format attempts
#' to be as simple as possible for non-technical collaborators.
#'
#' @param path A non-empty and non-NA character string. A path to a file to
#' read from, or write to.
#'
#' * This file must be a Translator file for [translator_read()].
#' * This file must be a translations file for [translations_read()].
#'
#' See Details for more information. [translator_write()] automatically
#' creates the parent directories of `path` (recursively) if they do not
#' exist.
#'
#' @param tr A [`Translator`][Translator] object.
#'
#' This argument is `NULL` by default for [translations_read()]. If a
#' [`Translator`][Translator] object is passed to this function, it
#' will read translations and further register them (as long as they
#' correspond to an existing source text).
#'
#' @param overwrite A non-NA logical value. Should existing files be
#' overwritten? If such files are detected and `overwrite` is set equal
#' to `TRUE`, an error is thrown.
#'
#' @param translations A non-NA logical value. Should translations files also
#' be read, or written along with `path` (the Translator file)?
#'
#' @param parent_dir A non-empty and non-NA character string. A path to a
#' parent directory.
#'
#' @template param-encoding
#'
#' @template param-lang
#'
#' @template param-verbose
#'
#' @returns
#' [translator_read()] returns an [`R6`][R6::R6] object of class
#' [`Translator`][Translator].
#'
#' [translator_write()] returns `NULL`, invisibly. It is used for its
#' side-effects of
#'
#' * creating a Translator file to the location given by `path`, and
#' * creating further translations file(s) in the same directory if
#' `translations` is `TRUE`.
#'
#' [translations_read()] returns an S3 object of class
#' [`ExportedTranslations`][export()].
#'
#' [translations_write()] returns `NULL`, invisibly.
#'
#' [translations_paths()] returns a named character vector.
#'
#' @seealso
#' [`Translator`][Translator],
#' [serialize()]
#'
#' @examples
#' # Set source language.
#' language_source_set("en")
#'
#' # Create a path to a temporary Translator file.
#' temp_path <- tempfile(pattern = "translator_", fileext = ".yml")
#' temp_dir <- dirname(temp_path) ## tempdir() could also be used
#'
#' # Create a Translator object.
#' # This would normally be done by find_source(), or translator_read().
#' tr <- translator(
#' id = "test-translator",
#' en = "English",
#' es = "Español",
#' fr = "Français",
#' text(
#' en = "Hello, world!",
#' fr = "Bonjour, monde!"),
#' text(
#' en = "Farewell, world!",
#' fr = "Au revoir, monde!"))
#'
#' # Export it. This creates 3 files: 1 Translator file, and 2 translations
#' # files because two non-source languages are registered. The file for
#' # language "es" contains placeholders and must be completed.
#' translator_write(tr, temp_path)
#' translator_read(temp_path)
#'
#' # Translations can be read individually.
#' translations_files <- translations_paths(tr, temp_dir)
#' translations_read(translations_files[["es"]])
#' translations_read(translations_files[["fr"]])
#'
#' # This is rarely useful, but translations can also be exported individually.
#' # You may use this to add a new language, as long as it has an entry in the
#' # underlying Translator object (or file).
#' tr$set_native_languages(el = "Greek")
#'
#' translations_files <- translations_paths(tr, temp_dir)
#'
#' translations_write(tr, translations_files[["el"]], "el")
#' translations_read(file.path(temp_dir, "el.txt"))
#'
#' @rdname translator-io
#' @export
translator_read <- function(
path = getOption("transltr.path"),
encoding = "UTF-8",
verbose = getOption("transltr.verbose", TRUE),
translations = TRUE)
{
assert_lgl1(verbose)
assert_lgl1(translations)
string <- paste0(text_read(path, encoding), collapse = "\n")
tr <- deserialize(string)
# translations_paths() checks that tr has
# a single source language before reading
# translations files.
transl_files <- translations_paths(tr, dirname(path))
if (translations) {
lapply(transl_files, \(path) {
if (verbose) {
cat(sprintf("Reading translations from '%s'.", path), sep = "\n")
}
tryCatch({
# tr is updated by reference via import().
lang <- translations_read(path, encoding, tr)[["Language Code"]]
},
error = \(err) {
# Do not throw an error if something goes wrong
# when verbose is TRUE. Report the error as a
# console output and move on to the next file.
if (verbose) {
cat("Error(s): ", err$message, "\n", sep = "")
return(invisible())
}
# NOTE: this line of code is covered by 2 expectations
# in the test block "translator_read() reports errors",
# but covr sees it as being uncovered. Disabling its
# coverage until a fix is found.
stopf("in '%s': %s", path, err$message) # nocov
})
return(invisible())
})
}
return(tr)
}
#' @rdname translator-io
#' @export
translator_write <- function(
tr = translator(),
path = getOption("transltr.path"),
overwrite = FALSE,
verbose = getOption("transltr.verbose", TRUE),
translations = TRUE)
{
assert_chr1(path)
assert_lgl1(overwrite)
assert_lgl1(verbose)
assert_lgl1(translations)
if (!overwrite && file.exists(path)) {
stops("'path' already exists. Set 'overwrite' equal to 'TRUE'.")
}
if (!dir.exists(parent_dir <- dirname(path)) &&
!dir.create(parent_dir, FALSE, TRUE)) {
stops("parent directory of 'path' could not be created.")
}
# translations_paths() checks that tr is a
# Translator and has a single source language.
transl_paths <- translations_paths(tr, parent_dir)
if (translations) {
# Write Exported Translations (one per non-source
# native language) in the same directory as path.
map(path = transl_paths, lang = names(transl_paths), \(path, lang) {
if (verbose) {
cat(sprintf("Writing '%s' translations to '%s'.", lang, path),
sep = "\n")
}
translations_write(tr, path, lang)
})
}
comments <- c(
"# Translator",
"#",
"# - You may edit fields Identifier and Languages.",
"# - Do not edit other fields by hand. Edit source scripts instead.",
"%YAML 1.1",
"---")
text_write(c(comments, serialize(tr)), path)
return(invisible())
}
#' @rdname translator-io
#' @export
translations_read <- function(path = "", encoding = "UTF-8", tr = NULL) {
string <- paste0(text_read(path, encoding), collapse = "\n")
return(deserialize_translations(string, tr))
}
#' @rdname translator-io
#' @export
translations_write <- function(tr = translator(), path = "", lang = "") {
comments <- c(
"# Translations",
"#",
"# - Edit each 'Translation' subsection below.",
"# - Do not edit 'Source Text' subsections.",
"# - Choose UTF-8 whenever you have to select a character encoding.",
"# - You may use any text editor.",
"# - You may split long sentences with single new lines.",
"# - You may separate paragraphs by leaving a blank line between them.",
"# - You may include comments.",
"# - What follows an octothorpe (#) is ignored until the next line.",
"# - An escaped octothorpe (\\#) is treated as normal text.",
"")
text_write(c(comments, serialize_translations(tr, lang)), path)
return(invisible())
}
#' @rdname translator-io
#' @export
translations_paths <- function(
tr = translator(),
parent_dir = dirname(getOption("transltr.path")))
{
assert_chr1(parent_dir)
if (!is_translator(tr)) {
stops("'tr' must be a 'Translator' object.")
}
if (length(source_lang <- tr$source_langs) > 1L) {
stops("all 'Text' objects of 'tr' must have the same 'source_lang'.")
}
native_langs <- tr$native_languages
native_langs <- native_langs[names(native_langs) != tr$source_langs]
langs <- names(native_langs)
files <- file.path(parent_dir, sprintf("%s.txt", langs))
names(files) <- langs
return(files)
}