Files
AMR/R/translate.R
T

342 lines
16 KiB
R
Executable File

# ==================================================================== #
# TITLE: #
# AMR: An R Package for Working with Antimicrobial Resistance Data #
# #
# SOURCE CODE: #
# https://github.com/msberends/AMR #
# #
# PLEASE CITE THIS SOFTWARE AS: #
# Berends MS, Luz CF, Friedrich AW, et al. (2022). #
# AMR: An R Package for Working with Antimicrobial Resistance Data. #
# Journal of Statistical Software, 104(3), 1-31. #
# https://doi.org/10.18637/jss.v104.i03 #
# #
# Developed at the University of Groningen and the University Medical #
# Center Groningen in the Netherlands, in collaboration with many #
# colleagues from around the world, see our website. #
# #
# This R package is free software; you can freely use and distribute #
# it for both personal and commercial purposes under the terms of the #
# GNU General Public License version 2.0 (GNU GPL-2), as published by #
# the Free Software Foundation. #
# We created this package for both routine data analysis and academic #
# research and it was publicly released in the hope that it will be #
# useful, but it comes WITHOUT ANY WARRANTY OR LIABILITY. #
# #
# Visit our website for the full manual and a complete tutorial about #
# how to conduct AMR data analysis: https://amr-for-r.org #
# ==================================================================== #
#' Translate Strings from the AMR Package
#'
#' For language-dependent output of `AMR` functions, such as [mo_name()], [mo_gramstain()], [mo_type()] and [ab_name()].
#' @param x Text to translate.
#' @param language Language to choose. Use one of these supported language names or [ISO 639-1 codes](https://en.wikipedia.org/wiki/ISO_639-1): `r vector_or(paste0(sapply(LANGUAGES_SUPPORTED_NAMES, function(x) x[[1]]), " (" , LANGUAGES_SUPPORTED, ")"), quotes = FALSE, sort = FALSE)`.
#' @details The currently `r length(LANGUAGES_SUPPORTED)` supported languages are `r vector_and(paste0(sapply(LANGUAGES_SUPPORTED_NAMES, function(x) x[[1]]), " (" , LANGUAGES_SUPPORTED, ")"), quotes = FALSE, sort = FALSE)`. All these languages have translations available for all antimicrobial drugs and colloquial microorganism names.
#'
#' To permanently silence the once-per-session language note on a non-English operating system, you can set the package option [`AMR_locale`][AMR-options] in your `.Rprofile` file like this:
#'
#' ```r
#' # Open .Rprofile file
#' utils::file.edit("~/.Rprofile")
#'
#' # Then add e.g. Italian support to that file using:
#' options(AMR_locale = "Italian")
#' ```
#'
#' And then save the file.
#'
#' Please read about adding or updating a language in [our Wiki](https://github.com/msberends/AMR/wiki/).
#'
#' ### Changing the Default Language
#' The system language will be used at default, if that language is supported. The language to be used is determined in this order:
#'
#' 1. The package option [`AMR_locale`][AMR-options], set by using e.g. `set_AMR_locale("German")` or by running e.g. `options(AMR_locale = "German")`.
#'
#' Note that setting an \R option only works in the same session. Save the command `options(AMR_locale = "(your language)")` to your `.Rprofile` file to apply it for every session. Run `utils::file.edit("~/.Rprofile")` to edit your `.Rprofile` file.
#' 2. The system variable `LANG`, as returned by `Sys.getenv("LANG")`, e.g. set by adding `LANG="de_DE.utf8"` to your `.Renviron` file in your home directory.
#' 3. The system variable `LANGUAGE`, but only if `LANG` is not set. Note that `LANG` is set on most Linux and macOS systems, in which case `LANGUAGE` has no effect.
#' 4. The system locale, as returned by [`Sys.getlocale("LC_COLLATE")`][Sys.getlocale()], if neither `LANG` nor `LANGUAGE` is set.
#'
#' Thus, if the package option [`AMR_locale`][AMR-options] is set, the system variables `LANG` and `LANGUAGE` will be ignored.
#' @rdname translate
#' @name translate
#' @export
#' @examples
#' # Current settings (based on system language)
#' ab_name("Ciprofloxacin")
#' mo_name("Coagulase-negative Staphylococcus (CoNS)")
#'
#' # setting another language
#' set_AMR_locale("Dutch")
#' ab_name("Ciprofloxacin")
#' mo_name("Coagulase-negative Staphylococcus (CoNS)")
#'
#' # setting yet another language
#' set_AMR_locale("German")
#' ab_name("Ciprofloxacin")
#' mo_name("Coagulase-negative Staphylococcus (CoNS)")
#'
#' # set_AMR_locale() understands endonyms, English exonyms, and ISO 639-1:
#' set_AMR_locale("Deutsch")
#' set_AMR_locale("German")
#' set_AMR_locale("de")
#' ab_name("amox/clav")
#'
#' # reset to system default
#' reset_AMR_locale()
#' ab_name("amox/clav")
get_AMR_locale <- function() {
# a message for this will be thrown in translate_into_language() if outcome is non-English
if (!is.null(getOption("AMR_locale"))) {
return(validate_language(getOption("AMR_locale"), extra_txt = "set with `options(AMR_locale = ...)`"))
}
lang <- ""
# now check the system variables, of which LANG takes precedence over LANGUAGE
if (!identical("", Sys.getenv("LANGUAGE"))) {
lang <- Sys.getenv("LANGUAGE")
}
if (!identical("", Sys.getenv("LANG"))) {
lang <- Sys.getenv("LANG")
}
if (lang == "") {
lang <- Sys.getlocale("LC_COLLATE")
}
find_language(lang)
}
#' @rdname translate
#' @export
set_AMR_locale <- function(language) {
language <- validate_language(language)
options(AMR_locale = language)
if (interactive() || identical(Sys.getenv("IN_PKGDOWN"), "true")) {
# show which language to use now
message_(
"Using ", LANGUAGES_SUPPORTED_NAMES[[language]]$exonym,
ifelse(language != "en",
paste0(" (", LANGUAGES_SUPPORTED_NAMES[[language]]$endonym, ")"),
""
),
" for the AMR package for this session."
)
}
}
#' @rdname translate
#' @export
reset_AMR_locale <- function() {
options(AMR_locale = NULL)
if (interactive() || identical(Sys.getenv("IN_PKGDOWN"), "true")) {
# show which language to use now
language <- suppressMessages(get_AMR_locale())
message_("Using the ", LANGUAGES_SUPPORTED_NAMES[[language]]$exonym, " language (", LANGUAGES_SUPPORTED_NAMES[[language]]$endonym, ") for the AMR package for this session.")
}
}
#' @rdname translate
#' @export
translate_AMR <- function(x, language = get_AMR_locale()) {
translate_into_language(x,
language = language,
only_unknown = FALSE,
only_affect_ab_names = FALSE,
only_affect_mo_names = FALSE
)
}
validate_language <- function(language, extra_txt = character(0)) {
if (length(language) == 0 || isTRUE(trimws2(tolower(language[1])) %in% c("en", "english", "", "false", NA))) {
return("en")
} else if (language[1] %in% LANGUAGES_SUPPORTED) {
return(language[1])
}
lang <- find_language(language[1], fallback = FALSE)
stop_ifnot(length(lang) > 0 && lang %in% LANGUAGES_SUPPORTED,
"unsupported language for AMR package", extra_txt, ": \"", language, "\". Use one of these language names or ISO 639-1 codes: ",
paste0('"', vapply(FUN.VALUE = character(1), LANGUAGES_SUPPORTED_NAMES, function(x) x[[1]]),
'" ("', LANGUAGES_SUPPORTED, '")',
collapse = ", "
),
call = FALSE
)
lang
}
find_language <- function(language, fallback = TRUE) {
language <- Map(LANGUAGES_SUPPORTED_NAMES,
LANGUAGES_SUPPORTED,
f = function(l, n, check = language) {
grepl(
paste0(
"^(", l[1], "|", l[2], "|",
n, "(_|$)|", toupper(n), "(_|$))"
),
check,
ignore.case = TRUE,
perl = TRUE,
useBytes = FALSE
)
},
USE.NAMES = TRUE
)
language <- names(which(language == TRUE))
if (isTRUE(fallback) && length(language) == 0) {
# other language -> set to English
language <- "en"
}
language
}
# translate strings based on inst/translations.tsv
translate_into_language <- function(from,
language = get_AMR_locale(),
only_unknown = FALSE,
only_affect_ab_names = FALSE,
only_affect_mo_names = FALSE) {
# get ISO 639-1 of language
lang <- validate_language(language)
if (lang == "en") {
# don' translate
return(from)
}
trns <- get_translation_table(lang, only_unknown = only_unknown, only_affect_ab_names = only_affect_ab_names, only_affect_mo_names = only_affect_mo_names)
if (is.null(trns)) {
return(from)
}
from.bak <- from
from_unique <- unique(from)
# check if text to look for is in one of the patterns
any_form_in_patterns <- tryCatch(
# the same as `from_unique %like% trns$any_pattern`, but without validating the long pattern on every call
any(grepl(trns$any_pattern, tolower(from_unique), fixed = trns$any_pattern_fixed, perl = !trns$any_pattern_fixed)),
error = function(e) {
warning_("Translation not possible. Please create an issue at {.url https://github.com/msberends/AMR/issues}. Many thanks!")
return(FALSE)
}
)
if (!any_form_in_patterns) {
return(from)
}
from_unique_translated <- from_unique
# combinations like "Ticarcillin/clavulanic acid" are translated per part, unless the combination itself is in the translations
is_combination <- grepl("/", from_unique, fixed = TRUE) & !from_unique %in% trns$pattern_all
if (any(is_combination)) {
# all parts of all combinations are translated at once
parts <- strsplit(from_unique[is_combination], "/", fixed = TRUE)
parts_translated <- translate_into_language(unlist(parts, use.names = FALSE), language = lang, only_unknown = only_unknown, only_affect_ab_names = only_affect_ab_names, only_affect_mo_names = only_affect_mo_names)
parts_translated <- split(parts_translated, factor(rep(seq_along(parts), lengths(parts)), levels = seq_along(parts)))
from_unique_translated[is_combination] <- vapply(FUN.VALUE = character(1), parts_translated, paste, collapse = "/", USE.NAMES = FALSE)
}
if (any(!is_combination)) {
# every pattern is applied to all strings at once, in order of the translation table
terms <- from_unique[!is_combination]
for (i in seq_along(trns$pattern)) {
terms <- gsub(
pattern = trns$pattern[i],
replacement = trns$replacement[i],
x = terms,
ignore.case = trns$ignore_case[i],
fixed = trns$fixed[i],
perl = !trns$fixed[i]
)
}
# replace the placeholders by their translations
has_placeholder <- grepl("\001", terms, fixed = TRUE)
if (any(has_placeholder)) {
found <- gregexpr("\001[^\001\002]{3}\002", terms[has_placeholder], perl = TRUE)
regmatches(terms[has_placeholder], found) <- lapply(regmatches(terms[has_placeholder], found), function(p) trns$literal[match(p, trns$placeholder)])
}
from_unique_translated[!is_combination] <- terms
}
# force UTF-8 for diacritics
from_unique_translated <- enc2utf8(from_unique_translated)
# a kind of left join to get all results back
out <- from_unique_translated[match(from.bak, from_unique)]
if (!identical(from.bak, out) && get_AMR_locale() == lang && is.null(getOption("AMR_locale", default = NULL)) && message_not_thrown_before("translation", entire_session = TRUE) && interactive()) {
message(font_blue(word_wrap(
"Assuming the ", LANGUAGES_SUPPORTED_NAMES[[lang]]$exonym, " language (",
LANGUAGES_SUPPORTED_NAMES[[lang]]$endonym, ") for the AMR package. See `set_AMR_locale()` to change this or to silence this once-per-session note.",
as_note = TRUE
)))
}
out
}
# the translation table for a language and the settings of translate_into_language(), built once per session as TRANSLATIONS is fixed
get_translation_table <- function(lang, only_unknown = FALSE, only_affect_ab_names = FALSE, only_affect_mo_names = FALSE) {
key <- paste(lang, only_unknown, only_affect_ab_names, only_affect_mo_names, sep = "|")
if (!key %in% names(AMR_env$translation_tables)) {
AMR_env$translation_tables[key] <- list(build_translation_table(lang, only_unknown, only_affect_ab_names, only_affect_mo_names))
}
AMR_env$translation_tables[[key]]
}
build_translation_table <- function(lang, only_unknown, only_affect_ab_names, only_affect_mo_names) {
df_trans <- TRANSLATIONS # internal data file
# only keep lines where translation is available for this language
df_trans <- df_trans[which(!is.na(df_trans[, lang, drop = TRUE])), , drop = FALSE]
# and where the original string is not equal to the string in the target language
df_trans <- df_trans[which(df_trans[, "pattern", drop = TRUE] != df_trans[, lang, drop = TRUE]), , drop = FALSE]
if (only_unknown == TRUE) {
df_trans <- subset(df_trans, pattern %like% "unknown")
}
if (only_affect_ab_names == TRUE) {
df_trans <- subset(df_trans, affect_ab_name == TRUE)
}
if (only_affect_mo_names == TRUE) {
df_trans <- subset(df_trans, affect_mo_name == TRUE)
}
if (NROW(df_trans) == 0) {
return(NULL)
}
# default: case sensitive if value if 'case_sensitive' is missing:
df_trans$case_sensitive[is.na(df_trans$case_sensitive)] <- TRUE
# default: not using regular expressions if 'regular_expr' is missing:
df_trans$regular_expr[is.na(df_trans$regular_expr)] <- FALSE
# to check if text to look for is in one of the patterns
any_pattern <- paste0("(", paste(gsub(" +\\(.*", "", df_trans$pattern), collapse = "|"), ")")
if (only_affect_ab_names == TRUE) {
df_trans$pattern[df_trans$regular_expr == TRUE] <- paste0(df_trans$pattern[df_trans$regular_expr == TRUE], "$")
df_trans$pattern[df_trans$regular_expr == TRUE] <- gsub("$$", "$", df_trans$pattern[df_trans$regular_expr == TRUE], fixed = TRUE)
}
# sort trans file on length of pattern, so that the longest pattern is replaced first (word boundaries of regular expressions do not count)
trns <- df_trans[order(nchar(gsub("\\b", "", df_trans$pattern, fixed = TRUE)), decreasing = TRUE), , drop = FALSE]
# every translation is first replaced by a placeholder, so that a translation is never translated again, e.g. "Ticarcillin" to "Ticarcilline" and then to "Ticarcillinee"
# placeholders consist of ASCII control characters only, so that no pattern can match them
control_chars <- vapply(FUN.VALUE = character(1), 3:31, function(i) rawToChar(as.raw(i)))
placeholders <- paste0("\001", control_chars[(seq_len(nrow(trns)) - 1) %/% length(control_chars) %/% length(control_chars) + 1], control_chars[(seq_len(nrow(trns)) - 1) %/% length(control_chars) %% length(control_chars) + 1], control_chars[(seq_len(nrow(trns)) - 1) %% length(control_chars) + 1], "\002")
replacements <- trns[, lang, drop = TRUE]
# keep backreferences like "\\1" of regular expressions outside the placeholder
literals <- gsub("\\\\[0-9]", "", replacements)
replacements_placeholder <- ifelse(
literals == "",
replacements,
vapply(FUN.VALUE = character(1), seq_along(replacements), function(i) sub(literals[i], placeholders[i], replacements[i], fixed = TRUE))
)
list(
# all patterns, including combinations, to check whether a combination is translated as a whole
pattern_all = df_trans$pattern,
any_pattern = tolower(any_pattern),
any_pattern_fixed = !is_valid_regex(any_pattern),
pattern = trns$pattern,
replacement = replacements_placeholder,
ignore_case = !trns$case_sensitive & trns$regular_expr,
fixed = !trns$regular_expr,
placeholder = placeholders,
literal = literals
)
}