Add support for multiple languages (#125)

The implementation follows the tldr-pages client spec:
https://github.com/tldr-pages/tldr/blob/master/CLIENT-SPECIFICATION.md#language

Fixes #96
This commit is contained in:
Niklas Mohrin 2020-08-10 22:27:05 +02:00 committed by Danilo Bargen
commit 1db4b409bc
3 changed files with 142 additions and 37 deletions

View file

@ -2,7 +2,7 @@ use std::env;
use std::ffi::OsStr;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use app_dirs::{get_app_root, AppDataType};
use flate2::read::GzDecoder;
@ -142,41 +142,53 @@ impl Cache {
}
}
/// Check for pages for a given platform in one of the given languages.
fn find_page_for_platform(
name: &str,
cache_dir: &Path,
platform: &str,
language_dirs: &[String],
) -> Option<PathBuf> {
language_dirs
.iter()
.map(|lang_dir| cache_dir.join(lang_dir).join(platform).join(name))
.find(|path| path.exists() && path.is_file())
}
/// Search for a page and return the path to it.
pub fn find_page(&self, name: &str) -> Option<PathBuf> {
// Build page file name
pub fn find_page(&self, name: &str, languages: &[String]) -> Option<PathBuf> {
let page_filename = format!("{}.md", name);
// Get platform dir
let platforms_dir = match Self::get_cache_dir() {
Ok(cache_dir) => cache_dir.join("tldr-master").join("pages"),
let cache_dir = match Self::get_cache_dir() {
Ok(cache_dir) => cache_dir.join("tldr-master"),
Err(e) => {
log::error!("Could not get cache directory: {}", e);
return None;
}
};
// Determine platform
let platform = self.get_platform_dir();
let lang_dirs: Vec<String> = languages
.iter()
.map(|lang| {
if lang == "en" {
String::from("pages")
} else {
format!("pages.{}", lang)
}
})
.collect();
// Search for the page in the platform specific directory
if let Some(pf) = platform {
let path = platforms_dir.join(&pf).join(&page_filename);
if path.exists() && path.is_file() {
return Some(path);
// Try to find a platform specific path first.
if let Some(pf) = self.get_platform_dir() {
let pf_path = Self::find_page_for_platform(&page_filename, &cache_dir, pf, &lang_dirs);
if pf_path.is_some() {
return pf_path;
}
}
// If platform is not supported or if platform specific page does not exist,
// look up the page in the "common" directory.
let path = platforms_dir.join("common").join(&page_filename);
// Return it if it exists, otherwise give up and return `None`
if path.exists() && path.is_file() {
Some(path)
} else {
None
}
// Did not find platform specific results, fall back to "common"
Self::find_page_for_platform(&page_filename, &cache_dir, "common", &lang_dirs)
}
/// Return the available pages.

View file

@ -17,6 +17,7 @@
#[cfg(feature = "logging")]
extern crate env_logger;
use std::collections::HashSet;
use std::env;
use std::fs::File;
use std::io::BufRead;
@ -74,6 +75,7 @@ struct Args {
flag_seed_config: bool,
flag_markdown: bool,
flag_color: ColorOptions,
flag_language: Option<String>,
}
/// Print page by path
@ -255,6 +257,46 @@ fn get_os() -> OsType {
OsType::Other
}
fn get_languages(
env_lang: Result<String, std::env::VarError>,
env_language: Result<String, std::env::VarError>,
) -> Vec<String> {
// Language list according to
// https://github.com/tldr-pages/tldr/blob/master/CLIENT-SPECIFICATION.md#language
if let Ok(lang) = env_lang {
let language = env_language.unwrap_or_default();
let mut locales: Vec<&str> = language.split(':').collect();
locales.push(&lang);
locales.push("en");
let mut lang_list = Vec::new();
let mut found_languages = HashSet::new();
for locale in &locales {
if locale.len() >= 5 && locale.chars().nth(2) == Some('_') {
// Language with country code
let lang = &locale[..5];
if found_languages.insert(lang) {
lang_list.push(lang);
}
}
if locale.len() >= 2 && *locale != "POSIX" {
// Language code
let lang = &locale[..2];
if found_languages.insert(lang) {
lang_list.push(lang);
}
}
}
return lang_list.iter().map(|&s| String::from(s)).collect();
}
// Without the LANG environment variable, only English pages should be looked up.
vec!["en".into()]
}
fn main() {
// Initialize logger
init_log();
@ -382,8 +424,15 @@ fn main() {
check_cache(&args, enable_styles);
}
let languages = if let Some(ref lang) = args.flag_language {
// Language overwritten by console argument
vec![lang.clone()]
} else {
get_languages(std::env::var("LANG"), std::env::var("LANGUAGE"))
};
// Search for command in cache
if let Some(path) = cache.find_page(&command) {
if let Some(path) = cache.find_page(&command, &languages) {
if let Err(msg) = print_page(&path, args.flag_markdown, &config) {
eprintln!("{}", msg);
process::exit(1);
@ -409,7 +458,7 @@ fn main() {
#[cfg(test)]
mod test {
use crate::{Args, OsType, USAGE};
use crate::{get_languages, Args, OsType, USAGE};
use docopt::{Docopt, Error};
fn test_helper(argv: &[&str]) -> Result<Args, Error> {
@ -428,4 +477,47 @@ mod test {
let argv = vec!["cp", "--os", "lindows"];
assert!(!test_helper(&argv).is_ok());
}
#[test]
fn test_language_missing_lang_env() {
let lang_list = get_languages(Err(std::env::VarError::NotPresent), Ok("de:fr".into()));
assert_eq!(lang_list, vec!["en"]);
let lang_list = get_languages(
Err(std::env::VarError::NotPresent),
Err(std::env::VarError::NotPresent),
);
assert_eq!(lang_list, vec!["en"]);
}
#[test]
fn test_language_missing_language_env() {
let lang_list = get_languages(Ok("de".into()), Err(std::env::VarError::NotPresent));
assert_eq!(lang_list, vec!["de", "en"]);
}
#[test]
fn test_language_preference_order() {
let lang_list = get_languages(Ok("de".into()), Ok("fr:cn".into()));
assert_eq!(lang_list, vec!["fr", "cn", "de", "en"]);
}
#[test]
fn test_language_country_code_expansion() {
let lang_list = get_languages(Ok("pt_BR".into()), Err(std::env::VarError::NotPresent));
assert_eq!(lang_list, vec!["pt_BR", "pt", "en"]);
}
#[test]
fn test_language_ignore_posix_and_c() {
let lang_list = get_languages(Ok("POSIX".into()), Err(std::env::VarError::NotPresent));
assert_eq!(lang_list, vec!["en"]);
let lang_list = get_languages(Ok("C".into()), Err(std::env::VarError::NotPresent));
assert_eq!(lang_list, vec!["en"]);
}
#[test]
fn test_language_no_duplicates() {
let lang_list = get_languages(Ok("de".into()), Ok("fr:de:cn:de".into()));
assert_eq!(lang_list, vec!["fr", "de", "cn", "en"]);
}
}

View file

@ -5,19 +5,20 @@ Usage:
Options:
-h --help Show this screen
-v --version Show version information
-l --list List all commands in the cache
-f --render <file> Render a specific markdown file
-o --os <type> Override the operating system [linux, osx, sunos, windows]
-u --update Update the local cache
-c --clear-cache Clear the local cache
-p --pager Use a pager to page output
-m --markdown Display the raw markdown instead of rendering it
-q --quiet Suppress informational messages
--config-path Show config file path
--seed-config Create a basic config
--color <when> Control when to use color [always, auto, never] [default: auto]
-h --help Show this screen
-v --version Show version information
-l --list List all commands in the cache
-f --render <file> Render a specific markdown file
-o --os <type> Override the operating system [linux, osx, sunos, windows]
-L --language <lang> Override the language settings
-u --update Update the local cache
-c --clear-cache Clear the local cache
-p --pager Use a pager to page output
-m --markdown Display the raw markdown instead of rendering it
-q --quiet Suppress informational messages
--config-path Show config file path
--seed-config Create a basic config
--color <when> Control when to use color [always, auto, never] [default: auto]
Examples: