diff --git a/.gitignore b/.gitignore index aafe1ad..3d2af46 100644 --- a/.gitignore +++ b/.gitignore @@ -224,7 +224,7 @@ ClientBin/ *.publishsettings orleans.codegen.cs -# Including strong name files can present a security risk +# Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk @@ -320,7 +320,7 @@ __pycache__/ # OpenCover UI analysis results OpenCover/ -# Azure Stream Analytics local run output +# Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log @@ -329,7 +329,7 @@ ASALocalRun/ # NVidia Nsight GPU debugger configuration file *.nvuser -# MFractors (Xamarin productivity tool) working folder +# MFractors (Xamarin productivity tool) working folder mfractor/ #nyc @@ -339,4 +339,6 @@ coverage source dest -cache \ No newline at end of file +cache + +docs/public diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f42178..bb10148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ ## Unreleased +* Adds the ability to build the default language to the root path, rather than placing it under a language code. + * See [Docs > Build > Default language at root](https://rosey.app/docs/build/#default-language-at-root) for more information. + ## v2.1.1 (August 13, 2024) * Fixes the Windows release of Rosey via npm diff --git a/docs/content/docs/build.md b/docs/content/docs/build.md index bf10601..485b857 100644 --- a/docs/content/docs/build.md +++ b/docs/content/docs/build.md @@ -50,6 +50,21 @@ The default language for the site (i.e. the language of 'source.json'). Defaults |-------------------------------|--------------------------|--------------------| | `--default-language ` | `ROSEY_DEFAULT_LANGUAGE` | `default_language` | +### Default language at root + +Configures Rosey to retain input URLs for the default language. + +By default, Rosey will place the default language under a language code, e.g. `/en/index.html`, +and will generate a redirect file at `/index.html`. + +By setting this flag, Rosey will output the default language at the root path, e.g. `/index.html`. + +By setting this flag, Rosey will not generate any redirect pages. + +| CLI Flag | ENV Variable | Config Key | +|-------------------------------|----------------------------------|----------------------------| +| `--default-language-at-root` | `ROSEY_DEFAULT_LANGUAGE_AT_ROOT` | `default_language_at_root` | + ### Exclusions A regular expression used to determine which files not to copy as assets. Defaults to `\.(html?|json)$` @@ -70,6 +85,8 @@ The source folder that Rosey should look for translated images within. If omitte Path to a custom redirect template that Rosey should use for the base URL. +This option is ignored if you have set the "Default language at root" flag. + | CLI Flag | ENV Variable | Config Key | |--------------------------|-----------------------|-----------------| | `--redirect-page ` | `ROSEY_REDIRECT_PAGE` | `redirect_page` | diff --git a/docs/content/docs/urls.md b/docs/content/docs/urls.md index 5aec76a..596c4e5 100644 --- a/docs/content/docs/urls.md +++ b/docs/content/docs/urls.md @@ -7,6 +7,9 @@ weight: 6 Rosey URL locale files can contain translated URLs for your website in a given language. +If you just want to move one language to the root of the site, e.g. serve `/en/index.html` at `index.html` instead, see the +[Default language at root](/docs/build/#default-language-at-root) option for Rosey's build step. + ## Creating translated URL locale files Creating URL locale files is not a step performed by Rosey. This part of the translation workflow is left open ended, usually integrating into an existing translation workflow for a company, or being programmatically created by transforming the input URLs. @@ -51,7 +54,7 @@ The `rosey/locales/ja-jp.urls.json` locale file should match the structure: } ``` -Each of these keys is an object with `original` and `value` strings. The `value` string should contain the translated destination file, and will be used by Rosey when building your final multilingual site. +Each of these keys is an object with `original` and `value` strings. The `value` string should contain the translated destination file, and will be used by Rosey when building your final multilingual site. The output should always include the `.html` extension. Rosey will remove any trailing `index.html` filename where able. diff --git a/rosey/features/build/rosey-build-root-language.feature b/rosey/features/build/rosey-build-root-language.feature new file mode 100644 index 0000000..ad5fd5c --- /dev/null +++ b/rosey/features/build/rosey-build-root-language.feature @@ -0,0 +1,90 @@ +Feature: Rosey Build Using Root Language URLs + Background: + Given I have the environment variables: + | ROSEY_SOURCE | dist/site | + | ROSEY_DEST | dist/translated_site | + + Scenario: Rosey can build the default language to the root URLs + Given I have a "dist/site/index.html" file with the content: + """ + + +

english sentence

+ + + """ + And I have a "dist/site/about.html" file with the content: + """ + + +

Hello World

+ + + """ + And I have a "rosey/locales/blank.json" file with the content: + """ + { + "p": "------- --------" + } + """ + And I have a "rosey/locales/nada.json" file with the content: + """ + { + "p": "de nada" + } + """ + When I run my program with the flags: + | build | + | --default-language-at-root | + + Then I should not see the file "dist/translated_site/en/index.html" + Then I should not see the file "dist/translated_site/en/about.html" + + And I should see a selector 'p' in "dist/translated_site/index.html" with the attributes: + | data-rosey | p | + | innerText | english sentence | + And I should see a selector 'p' in "dist/translated_site/nada/index.html" with the attributes: + | data-rosey | p | + | innerText | de nada | + + Then I should see a selector 'h1>a' in "dist/translated_site/about.html" with the attributes: + | href | /posts/hello-world | + | innerText | Hello World | + + Then I should not see a selector 'link' in "dist/translated_site/about.html" with the attributes: + | rel | alternate | + | href | /en/about.html | + | hreflang | en | + Then I should see a selector 'link' in "dist/translated_site/about.html" with the attributes: + | rel | alternate | + | href | /blank/about.html | + | hreflang | blank | + Then I should see a selector 'link' in "dist/translated_site/about.html" with the attributes: + | rel | alternate | + | href | /nada/about.html | + | hreflang | nada | + + Then I should see a selector 'link' in "dist/translated_site/blank/about.html" with the attributes: + | rel | alternate | + | href | /about.html | + | hreflang | en | + Then I should see a selector 'link' in "dist/translated_site/blank/about.html" with the attributes: + | rel | alternate | + | href | /nada/about.html | + | hreflang | nada | + + Then I should see a selector 'link' in "dist/translated_site/blank/index.html" with the attributes: + | rel | alternate | + | href | / | + | hreflang | en | + Then I should see a selector 'link' in "dist/translated_site/blank/index.html" with the attributes: + | rel | alternate | + | href | /nada/ | + | hreflang | nada | + + Then I should see a selector 'meta' in "dist/translated_site/index.html" with the attributes: + | http-equiv | content-language | + | content | en | + Then I should see a selector 'meta' in "dist/translated_site/blank/index.html" with the attributes: + | http-equiv | content-language | + | content | blank | diff --git a/rosey/src/lib.rs b/rosey/src/lib.rs index 0c32983..3ac3be3 100644 --- a/rosey/src/lib.rs +++ b/rosey/src/lib.rs @@ -76,7 +76,9 @@ impl RoseyOptions { std::process::exit(1); }); - if !matches!(subcommand, RoseyCommand::Check) && original_source.to_string_lossy().is_empty() { + if !matches!(subcommand, RoseyCommand::Check) + && original_source.to_string_lossy().is_empty() + { eprintln!( "Rosey requires a source directory to process. Provide either: \n\ • A `--source ` CLI flag \n\ @@ -109,6 +111,7 @@ impl RoseyOptions { base: working_dir.join(matches.get("base", base.base)), base_urls: working_dir.join(matches.get("base-urls", base.base_urls)), default_language: matches.get("default-language", base.default_language), + default_language_at_root: matches.is_present("default-language-at-root") || base.default_language_at_root, redirect_page: matches .get_opt("redirect-page", base.redirect_page) .map(|p| working_dir.join(p)), @@ -117,8 +120,8 @@ impl RoseyOptions { .get_opt("images-source", base.images_source) .map(|p| working_dir.join(p)), wrap: match matches.values_of("wrap") { - Some(langs) => Some(langs.map(|l|{ - + Some(langs) => Some(langs.map(|l|{ + if !SUPPORTED_WRAP_LANGS.iter().any(|lang| l.starts_with(lang)) { eprintln!("Cannot wrap text for language '{l}'. Languages with supported text wrapping: {SUPPORTED_WRAP_LANGS:?}"); std::process::exit(1); @@ -338,4 +341,4 @@ pub fn inline_templates(dom: &kuchiki::NodeRef) { node.append(contents.clone()); } }) -} \ No newline at end of file +} diff --git a/rosey/src/main.rs b/rosey/src/main.rs index d546ce3..ae17c0d 100644 --- a/rosey/src/main.rs +++ b/rosey/src/main.rs @@ -151,6 +151,13 @@ async fn main() { .takes_value(true) .help("The source folder that Rosey should look for translated images within. \n ─ Defaults to the source folder"), ) + .arg( + Arg::with_name("default-language-at-root") + .long("default-language-at-root") + .takes_value(false) + .conflicts_with("redirect-page") + .help("Configures Rosey to leave all input URLs in-place for the default language, and omit generating redirect files"), + ) .arg( Arg::with_name("redirect-page") .long("redirect-page") diff --git a/rosey/src/options.rs b/rosey/src/options.rs index c3cdcee..fddd66c 100644 --- a/rosey/src/options.rs +++ b/rosey/src/options.rs @@ -22,6 +22,7 @@ pub struct RoseyPublicConfig { pub images_source: Option, pub default_language: String, pub redirect_page: Option, + pub default_language_at_root: bool, pub wrap: Option>, pub wrap_class: Option, pub verbose: bool, @@ -43,6 +44,7 @@ impl Default for RoseyPublicConfig { images_source: None, default_language: "en".into(), redirect_page: None, + default_language_at_root: false, wrap: None, wrap_class: None, verbose: false, @@ -73,6 +75,7 @@ impl Display for RoseyPublicConfig { writeln!(f, " - Base locale file: {}", self.base.display())?; writeln!(f, " - Base urls file: {}", self.base_urls.display())?; writeln!(f, " - Locales directory: {}", self.locales.display())?; + match &self.images_source { Some(s) => writeln!(f, " - Images source: {}", s.display())?, None => writeln!( @@ -80,12 +83,24 @@ impl Display for RoseyPublicConfig { " - Images source: * unset, using source directory *" )?, } - match &self.redirect_page { - Some(s) => writeln!(f, " - Redirect page: {}", s.display())?, - None => writeln!( + + if self.default_language_at_root { + writeln!(f, " - Root URLs: Default language")?; + + writeln!( f, - " - Redirect page: * unset, using default redirect template *" - )?, + " - Redirect page: * ignored, root urls are default language *" + )? + } else { + writeln!(f, " - Root URLs: Generated redirect page")?; + + match &self.redirect_page { + Some(s) => writeln!(f, " - Redirect page: {}", s.display())?, + None => writeln!( + f, + " - Redirect page: * unset, using default redirect template *" + )?, + } } writeln!(f, " Options:")?; diff --git a/rosey/src/runners/builder/html.rs b/rosey/src/runners/builder/html.rs index 7f7ccf3..4a42839 100644 --- a/rosey/src/runners/builder/html.rs +++ b/rosey/src/runners/builder/html.rs @@ -50,6 +50,7 @@ impl RoseyBuilder { &config.tag, images_source.to_owned(), &config.default_language, + config.default_language_at_root, &self.translations, &config.wrap, &config.wrap_class, @@ -82,16 +83,21 @@ impl RoseyBuilder { .map(Into::into) .unwrap_or_else(|| relative_path.to_owned()); - let output_path = dest_folder - .join(&config.default_language) - .join(translated_default_url); - page.output_file(&output_path); + if config.default_language_at_root { + let output_path = dest_folder.join(translated_default_url); + page.output_file(&output_path); + } else { + let output_path = dest_folder + .join(&config.default_language) + .join(translated_default_url); + page.output_file(&output_path); - self.output_redirect_file( - &config.default_language, - relative_path, - &self.url_translations, - ); + self.output_redirect_file( + &config.default_language, + relative_path, + &self.url_translations, + ); + } self.translations.keys().for_each(|key| { let url_translations = self.url_translations.get(key); @@ -216,6 +222,7 @@ struct RoseyPage<'a> { assets: Vec<(String, String, NodeRef)>, locale_key: Option<&'a str>, should_wrap: bool, + default_language_at_root: bool, wrap: &'a Option>, wrap_class: &'a Option, pub tag: String, @@ -232,6 +239,7 @@ impl<'a> RoseyPage<'a> { tag: &str, images_source: PathBuf, default_language: &str, + default_language_at_root: bool, translations: &'a BTreeMap, wrap: &'a Option>, wrap_class: &'a Option, @@ -255,6 +263,7 @@ impl<'a> RoseyPage<'a> { translations, locale_key: None, should_wrap: false, + default_language_at_root, wrap, wrap_class, } @@ -344,8 +353,15 @@ impl<'a> RoseyPage<'a> { let output = parsed.as_str().trim_start_matches(base_url.as_str()); + let output_href = + if locale_key == self.default_language && self.default_language_at_root { + format!("/{output}") + } else { + format!("/{locale_key}/{output}") + }; + attributes.remove("href"); - attributes.insert("href", format!("/{locale_key}/{output}")); + attributes.insert("href", output_href); } } @@ -605,10 +621,16 @@ impl<'a> RoseyPage<'a> { attributes.insert("hreflang", String::from(key)); } + let output_href = if key == self.default_language && self.default_language_at_root { + format!("/{translated_path}") + } else { + format!("/{key}/{translated_path}") + }; + if let Some(href) = attributes.get_mut("href") { - *href = format!("/{key}/{translated_path}"); + *href = output_href; } else { - attributes.insert("href", format!("/{key}/{translated_path}")); + attributes.insert("href", output_href); } } }