temp
This commit is contained in:
48
dioxus-i18n/.github/release-drafter.yml
vendored
Normal file
48
dioxus-i18n/.github/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name-template: "Release v$RESOLVED_VERSION 🦀"
|
||||
tag-template: "v$RESOLVED_VERSION"
|
||||
categories:
|
||||
- title: "🚀 Features"
|
||||
label: "feature"
|
||||
- title: "🐛 Bug Fixes"
|
||||
label: "bug"
|
||||
- title: "♻️ Refactor"
|
||||
label: "refactor"
|
||||
- title: "📝 Documentation"
|
||||
label: "documentation"
|
||||
- title: "🧰 Maintenance"
|
||||
labels:
|
||||
- "chore"
|
||||
- "dependencies"
|
||||
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
|
||||
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- "major"
|
||||
minor:
|
||||
labels:
|
||||
- "minor"
|
||||
patch:
|
||||
labels:
|
||||
- "patch"
|
||||
default: patch
|
||||
template: |
|
||||
## Changes
|
||||
|
||||
$CHANGES
|
||||
autolabeler:
|
||||
- label: feature
|
||||
branch:
|
||||
- "/^feat(ure)?[/-].+/"
|
||||
- label: bug
|
||||
branch:
|
||||
- "/^fix[/-].+/"
|
||||
- label: refactor
|
||||
branch:
|
||||
- "/(refactor|refactoring)[/-].+/"
|
||||
- label: documentation
|
||||
branch:
|
||||
- "/doc(s|umentation)[/-].+/"
|
||||
- label: chore
|
||||
branch:
|
||||
- "/^chore[/-].+/"
|
||||
19
dioxus-i18n/.github/workflows/publish.yml
vendored
Normal file
19
dioxus-i18n/.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Cargo Publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish to crate.io
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: cargo publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
19
dioxus-i18n/.github/workflows/release-drafter.yml
vendored
Normal file
19
dioxus-i18n/.github/workflows/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
40
dioxus-i18n/.github/workflows/test_runs.yml
vendored
Normal file
40
dioxus-i18n/.github/workflows/test_runs.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Test Runs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Updates
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
libglib2.0-dev
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Lint
|
||||
run: cargo clippy -- -D warnings
|
||||
|
||||
- name: Test
|
||||
run: cargo test
|
||||
|
||||
- name: Compile
|
||||
run: |
|
||||
rustup target add wasm32-unknown-unknown
|
||||
cargo build --target wasm32-unknown-unknown
|
||||
cargo build --release
|
||||
2
dioxus-i18n/.gitignore
vendored
Normal file
2
dioxus-i18n/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
Cargo.lock
|
||||
/target
|
||||
117
dioxus-i18n/CHANGELOG.md
Normal file
117
dioxus-i18n/CHANGELOG.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Changelog
|
||||
|
||||
## [0.4.3]
|
||||
|
||||
- [Issue #19](https://github.com/dioxus-community/dioxus-i18n/issues/19) Enable use of "message-id.attribute-id"
|
||||
syntax in the `t!`, `te!`, `tid!` macros in order to extract attribute definition, e.g. `t!("mycomponent.placeholder")`
|
||||
in:
|
||||
```
|
||||
mycomponent = Component Name
|
||||
.placeholder = Some placeholder
|
||||
.aria-text = Some aria text
|
||||
```
|
||||
|
||||
- Added examples for all fluent grammar constructs and configuration variants.
|
||||
|
||||
## [0.4.2] 2025-02-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- [Issue #15](https://github.com/dioxus-community/dioxus-i18n/issues/15) Recent change to t! macro unnecessarily breaks v0.3 code.
|
||||
|
||||
### Amended
|
||||
|
||||
- t! macro amended to use unwrap_or_else rather than panic!.
|
||||
|
||||
- Error messages made consistant across all macros.
|
||||
|
||||
## [0.4.1] 2025-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- New methods (`I18nConfig::with_auto_locales`) to determine supported locales from deep search for translation files.
|
||||
|
||||
- New methods returning `Result<_, Error>` rather than `panic!`, such that:
|
||||
| __`panic!` version__ | __`Result<_, Error>` vesion__ |
|
||||
|-----------------------------------------|------------------------------------------|
|
||||
| `LocaleResource::to_resource_string` | `LocaleResource::try_to_resource_string` |
|
||||
| `I18n::translate` | `I18n::try_translate` |
|
||||
| `I18n::translate_with_args` | `I18n::try_translate_with_args` |
|
||||
| `I18n::set_fallback_language` | `I18n::try_set_fallback_language` |
|
||||
| `I18n::set_language` | `I18n::try_set_language` |
|
||||
| `use_init_i18n` | `try_use_init_i18n` |
|
||||
| `I18nConfig::with_auto_locales` | `I18nConfig::try_with_auto_locales` |
|
||||
|
||||
- New `te!` macro which acts like `t!` but returns `Error`.
|
||||
|
||||
- New `tid!` macro which acts like `t!` but returns the message-id.
|
||||
|
||||
### Change
|
||||
|
||||
- t! macro amended to use `try_translate` and `try_translate_with_args`, but will perform `.expect("..")`
|
||||
and therefore panic! on error. This retains backwards compatibility for this macro.
|
||||
|
||||
- Use of `set_fallback_language` / `try_set_fallback_language` without a corresponding locale
|
||||
translation is treated as an error.
|
||||
|
||||
## [0.4.0] 2025-01-25
|
||||
|
||||
### Added
|
||||
|
||||
- Code:
|
||||
- Doc comments
|
||||
- Module tests for `cargo test`
|
||||
|
||||
- Amended `I18nConfig::with_locale` so that the `Locale` dynamic or static
|
||||
constructors no longer have to be _explicitly_ given.
|
||||
They can be determined implicitly from `(LanguageIdentifier, &str)` or
|
||||
`(LanguageIdentifer, PathBuf)`.
|
||||
|
||||
- Enabled shared 'LocaleResource's, where two dialect can use the same translation file.
|
||||
For example ["en", "en-GB"] share "en-GB.ftl".
|
||||
|
||||
### Changed
|
||||
|
||||
- The translations used are determined when `I18n::set_language` or
|
||||
`I18n::set_fallback_language` is called, and not each time a message is translated.
|
||||
|
||||
- __Fallback handling has changed__. It no longer just uses _fallback_language_ when the message
|
||||
id is missing from the current _locale_. It performs a graceful fallback from
|
||||
_<language>-<region>_ to _<language>_ before using the actual _fallback_ (in fact it
|
||||
falls back along the _<language>-<optionalScript>-<optionalRegion>-<optionalVariants>_
|
||||
hiearchy).
|
||||
|
||||
__Note:__ this is a breaking change which may impact the selected translation.
|
||||
|
||||
- `LocaleResource::to_string` renamed to `LocaleResource::to_resource_string`
|
||||
|
||||
## [0.3.0] 2024-12-10
|
||||
|
||||
- [Dioxus 0.6](https://dioxuslabs.com/) support
|
||||
|
||||
## [0.2.4] 2024-09-11
|
||||
|
||||
- Hide new_dynamic in WASM
|
||||
- New t!() macro
|
||||
|
||||
## [0.2.3] 2024-09-04
|
||||
|
||||
- Support dynamic loading of locales
|
||||
|
||||
## [0.2.2] 2024-09-02
|
||||
|
||||
- Enable macros instead of serde in unic-langid
|
||||
|
||||
## [0.2.1] 2024-09-02
|
||||
|
||||
- Export unic_langid and fluent
|
||||
- Use absolute path to import fluent in the translate macro
|
||||
- Updated freya example
|
||||
|
||||
## [0.2.0] 2024-09-01
|
||||
|
||||
- Now based in the [Fluent Project](https://github.com/projectfluent/fluent-rs)
|
||||
|
||||
## [0.1.0] 2024-08-31
|
||||
|
||||
- Initial release
|
||||
30
dioxus-i18n/Cargo.toml
Normal file
30
dioxus-i18n/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "dioxus-i18n"
|
||||
version = "0.5.1"
|
||||
edition = "2021"
|
||||
authors = ["Marc Espín <mespinsanz@gmail.com>"]
|
||||
description = "i18n integration for Dioxus apps based on Fluent Project."
|
||||
license = "MIT"
|
||||
repository = "https://github.com/dioxus-community/dioxus-i18n"
|
||||
readme = "./README.md"
|
||||
categories = ["accessibility", "gui", "localization", "internationalization"]
|
||||
|
||||
[dependencies]
|
||||
dioxus = { git = "https://github.com/matous-volf/dioxus", rev = "627d5ca5b80aeed57c23e253024665f103117f5e", default-features = false, features = [
|
||||
"hooks",
|
||||
"macro",
|
||||
"signals",
|
||||
] }
|
||||
fluent = "0.17"
|
||||
thiserror = "2.0"
|
||||
unic-langid = { version = "0.9", features = ["macros"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
walkdir = "2.5.0"
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { git = "https://github.com/matous-volf/dioxus", rev = "627d5ca5b80aeed57c23e253024665f103117f5e", features = ["desktop"] }
|
||||
freya = "0.3"
|
||||
futures = "0.3.31"
|
||||
pretty_assertions = "1.4.1"
|
||||
unic-langid = { version = "0.9.5", features = ["macros"] }
|
||||
20
dioxus-i18n/LICENSE.md
Normal file
20
dioxus-i18n/LICENSE.md
Normal file
@@ -0,0 +1,20 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Marc Espín Sanz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
85
dioxus-i18n/README.md
Normal file
85
dioxus-i18n/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# dioxus-i18n 🌍
|
||||
|
||||
i18n integration for Dioxus apps based on the [Project Fluent](https://github.com/projectfluent/fluent-rs).
|
||||
|
||||
> This crate used to be in the [Dioxus SDK](https://github.com/DioxusLabs/sdk).
|
||||
|
||||
## Support
|
||||
|
||||
- **Dioxus v0.6** 🧬
|
||||
- Renderers:
|
||||
- [web](https://dioxuslabs.com/learn/0.6/guides/web/),
|
||||
- [desktop](https://dioxuslabs.com/learn/0.6/guides/desktop/),
|
||||
- [freya](https://github.com/marc2332/freya)
|
||||
- Both WASM and native targets
|
||||
|
||||
## Example:
|
||||
|
||||
```ftl
|
||||
# en-US.ftl
|
||||
|
||||
hello = Hello, {$name}!
|
||||
```
|
||||
|
||||
```rs
|
||||
// main.rs
|
||||
|
||||
fn app() -> Element {
|
||||
let i18 = use_init_i18n(|| {
|
||||
I18nConfig::new(langid!("en-US"))
|
||||
// implicit [`Locale`]
|
||||
.with_locale(( // Embed
|
||||
langid!("en-US"),
|
||||
include_str!("./en-US.ftl")
|
||||
))
|
||||
.with_locale(( // Load at launch
|
||||
langid!("es-ES"),
|
||||
PathBuf::from("./es-ES.ftl"),
|
||||
))
|
||||
.with_locale(( // Locales will share duplicated locale_resources
|
||||
langid!("en"), // which is useful to assign a specific region for
|
||||
include_str!("./en-US.ftl") // the primary language
|
||||
))
|
||||
// explicit [`Locale`]
|
||||
.with_locale(Locale::new_static( // Embed
|
||||
langid!("en-US"),
|
||||
include_str!("./en-US.ftl"),
|
||||
))
|
||||
.with_locale(Locale::new_dynamic( // Load at launch
|
||||
langid!("es-ES"),
|
||||
PathBuf::from("./es-ES.ftl"),
|
||||
))
|
||||
});
|
||||
|
||||
rsx!(
|
||||
label { { t!("hello", name: "World") } }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Further examples
|
||||
|
||||
The examples folder contains a number of working examples:
|
||||
|
||||
* Desktop examples:
|
||||
* [Dioxus](./examples/dioxus-desktop.rs)
|
||||
* [Freya](./examples/freya.rs)
|
||||
* Configuration variants:
|
||||
* [Auto locales](./examples/config-auto-locales.rs)
|
||||
* [Dynamic (PathBuf)](./examples/config-dynamic-pathbuf.rs)
|
||||
* [Static (include_str!)](./examples/config-static-includestr.rs)
|
||||
* Fluent grammer:
|
||||
* [Application](./examples/fluent-grammar.rs)
|
||||
* [FTL file](./examples/data/fluent/en.ftl)
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Checks clean compile against `#[cfg(not(target_arch = "wasm32"))]`
|
||||
cargo build --target wasm32-unknown-unknown
|
||||
|
||||
# Runs all tests
|
||||
cargo test
|
||||
```
|
||||
|
||||
[MIT License](./LICENSE.md)
|
||||
47
dioxus-i18n/examples/config-auto-locales.rs
Normal file
47
dioxus-i18n/examples/config-auto-locales.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! This example demonstrates how to use an auto_locales derived I18nConfig.
|
||||
//! This is useful when you have a lot of locales and you don't want to manually add them.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_i18n::{prelude::*, t};
|
||||
use unic_langid::langid;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
launch(app);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn Body() -> Element {
|
||||
let mut i18n = i18n();
|
||||
|
||||
let change_to_english = move |_| i18n.set_language(langid!("en-US"));
|
||||
let change_to_spanish = move |_| i18n.set_language(langid!("es-ES"));
|
||||
|
||||
rsx!(
|
||||
button {
|
||||
onclick: change_to_english,
|
||||
label {
|
||||
"English"
|
||||
}
|
||||
}
|
||||
button {
|
||||
onclick: change_to_spanish,
|
||||
label {
|
||||
"Spanish"
|
||||
}
|
||||
}
|
||||
p { { t!("hello_world") } }
|
||||
p { { t!("hello", name: "Dioxus") } }
|
||||
)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
use_init_i18n(|| {
|
||||
// This initialisation performs a deep search for all locales in the given path.
|
||||
// It IS NOT supported in WASM targets.
|
||||
I18nConfig::new(langid!("en-US")).with_auto_locales(PathBuf::from("./examples/data/i18n/"))
|
||||
});
|
||||
|
||||
rsx!(Body {})
|
||||
}
|
||||
62
dioxus-i18n/examples/config-dynamic-pathbuf.rs
Normal file
62
dioxus-i18n/examples/config-dynamic-pathbuf.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
//! This example demonstrates how to use pathbuf derived I18nConfig.
|
||||
//! This is useful when the path to the translation files is not known at compile time.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_i18n::{prelude::*, t};
|
||||
use unic_langid::langid;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
launch(app);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn Body() -> Element {
|
||||
let mut i18n = i18n();
|
||||
|
||||
let change_to_english = move |_| i18n.set_language(langid!("en-US"));
|
||||
let change_to_spanish = move |_| i18n.set_language(langid!("es-ES"));
|
||||
|
||||
rsx!(
|
||||
button {
|
||||
onclick: change_to_english,
|
||||
label {
|
||||
"English"
|
||||
}
|
||||
}
|
||||
button {
|
||||
onclick: change_to_spanish,
|
||||
label {
|
||||
"Spanish"
|
||||
}
|
||||
}
|
||||
p { { t!("hello_world") } }
|
||||
p { { t!("hello", name: "Dioxus") } }
|
||||
)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
use_init_i18n(|| {
|
||||
// This initialisation allows individual translation files to be selected.
|
||||
// The locales can be added with an implicitly derived locale (see config-static-includestr.rs for a comparison)
|
||||
// or using an explicit Locale::new_dynamic call.
|
||||
//
|
||||
// The two examples are functionally equivalent.
|
||||
//
|
||||
// It IS NOT supported in WASM targets.
|
||||
I18nConfig::new(langid!("en-US"))
|
||||
// Implicit...
|
||||
.with_locale((
|
||||
langid!("es-ES"),
|
||||
PathBuf::from("./examples/data/i18n/es-ES.ftl"),
|
||||
))
|
||||
// Explicit...
|
||||
.with_locale(Locale::new_dynamic(
|
||||
langid!("en-US"),
|
||||
PathBuf::from("./examples/data/i18n/en-US.ftl"),
|
||||
))
|
||||
});
|
||||
|
||||
rsx!(Body {})
|
||||
}
|
||||
57
dioxus-i18n/examples/config-static-includestr.rs
Normal file
57
dioxus-i18n/examples/config-static-includestr.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
//! This example demonstrates how to use pathbuf derived I18nConfig.
|
||||
//! This is useful for WASM targets; the paths to the translation files must be known at compile time.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_i18n::{prelude::*, t};
|
||||
use unic_langid::langid;
|
||||
|
||||
fn main() {
|
||||
launch(app);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn Body() -> Element {
|
||||
let mut i18n = i18n();
|
||||
|
||||
let change_to_english = move |_| i18n.set_language(langid!("en-US"));
|
||||
let change_to_spanish = move |_| i18n.set_language(langid!("es-ES"));
|
||||
|
||||
rsx!(
|
||||
button {
|
||||
onclick: change_to_english,
|
||||
label {
|
||||
"English"
|
||||
}
|
||||
}
|
||||
button {
|
||||
onclick: change_to_spanish,
|
||||
label {
|
||||
"Spanish"
|
||||
}
|
||||
}
|
||||
p { { t!("hello_world") } }
|
||||
p { { t!("hello", name: "Dioxus") } }
|
||||
)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
use_init_i18n(|| {
|
||||
// This initialisation allows individual translation files to be selected.
|
||||
// The locales can be added with an implicitly derived locale (see config-dynamic-pathbuf.rs for a comparison)
|
||||
// or using an explicit Locale::new_static call.
|
||||
//
|
||||
// The two examples are functionally equivalent.
|
||||
//
|
||||
// It IS supported in WASM targets.
|
||||
I18nConfig::new(langid!("en-US"))
|
||||
// Implicit...
|
||||
.with_locale((langid!("es-ES"), include_str!("./data/i18n/es-ES.ftl")))
|
||||
// Explicit...
|
||||
.with_locale(Locale::new_static(
|
||||
langid!("en-US"),
|
||||
include_str!("./data/i18n/en-US.ftl"),
|
||||
))
|
||||
});
|
||||
|
||||
rsx!(Body {})
|
||||
}
|
||||
124
dioxus-i18n/examples/data/fluent/en.ftl
Normal file
124
dioxus-i18n/examples/data/fluent/en.ftl
Normal file
@@ -0,0 +1,124 @@
|
||||
### Fluent grammar examples for dioxus-i18n.
|
||||
|
||||
## These examples demonstrate Fluent file grammar and how dioxus-i18n can be
|
||||
## used to access these translations.
|
||||
|
||||
## Examples derived from: https://projectfluent.org/fluent/guide/index.html
|
||||
|
||||
# Simple message
|
||||
simple-message = This is a simple message.
|
||||
|
||||
# $name (String) - The name you want to display.
|
||||
message-with-variable = This is a message with a variable: { $name }.
|
||||
|
||||
# Reference to a term.
|
||||
-a-term = This is a common term used by many messages.
|
||||
message-referencing-a-term = This is a message with a reference: { -a-term }.
|
||||
|
||||
# Use of special characters.
|
||||
message-with-special-character = This message contain opening curly brace {"{"} and a closing curly brace {"}"}.
|
||||
|
||||
# Message with blanks.
|
||||
blank-is-removed = This message starts with no blanks.
|
||||
blank-is-preserved = {" "}This message starts with 4 spaces (note HTML contracts them).
|
||||
|
||||
# Message with attributes.
|
||||
message-with-attributes = Predefined value
|
||||
.placeholder = email@example.com
|
||||
.aria-label = Login input value
|
||||
.title = Type your login email
|
||||
|
||||
# Message with quotes.
|
||||
literal-quote-cryptic = Text in {"\""}double quotes{"\""}.
|
||||
literal-quote-preferred = Text in "double quotes".
|
||||
|
||||
# Message with Unicode characters.
|
||||
unicode-cryptic = {"\u2605"} {"\u2606"} {"\u2728"} {"\u262F"} {"\u263A"}
|
||||
unicode-preferred = ★ ☆ ✨ ☯ ☺
|
||||
|
||||
# Message with a placeable.
|
||||
single-line = Text can be written in a single line.
|
||||
|
||||
multi-line = Text can also span multiple lines
|
||||
as long as each new line is indented
|
||||
by at least one space.
|
||||
|
||||
block-line =
|
||||
Sometimes it's more readable to format
|
||||
multiline text as a "block", which means
|
||||
starting it on a new line. All lines must
|
||||
be indented by at least one space.
|
||||
|
||||
# Message using functions.
|
||||
#
|
||||
# Note: Builtin functions are currently unsupported: See Fluent issue https://github.com/projectfluent/fluent-rs/issues/181
|
||||
# The Bundle::add_builtins() function is not published at the time of writing this example.
|
||||
#
|
||||
# Using a builtin currently results in an error.
|
||||
#
|
||||
# $duration (Number) - The duration in seconds.
|
||||
time-elapsed-no-function = Time elapsed: { $duration }s.
|
||||
time-elapsed-function = Currently unsupported: error raised: { NUMBER($duration) }.
|
||||
|
||||
# Message reference.
|
||||
referenced-message = Referenced message
|
||||
message-referencing-another-message = Message referencing another message: { referenced-message }.
|
||||
|
||||
# Message selection plurals.
|
||||
message-selection-plurals =
|
||||
{ $value ->
|
||||
*[one] Value is one: { $value }.
|
||||
[other] Value is more than one: { $value }.
|
||||
}
|
||||
|
||||
# Message selection numeric.
|
||||
# Argument must be numeric.
|
||||
message-selection-numeric =
|
||||
{ NUMERIC($value) ->
|
||||
[0.0] Zero: { $value }.
|
||||
*[0.5] A half: { $value }.
|
||||
[other] Other ($value)
|
||||
}
|
||||
|
||||
# Message selection number.
|
||||
#
|
||||
# Note: Builtin functions are currently unsupported: See Fluent issue https://github.com/projectfluent/fluent-rs/issues/181
|
||||
# The Bundle::add_builtins() function is not published at the time of writing this example.
|
||||
#
|
||||
# Using the NUMBER builtin always results in a default behaviour.
|
||||
#
|
||||
message-selection-number = { NUMBER($pos, type: "ordinal") ->
|
||||
[1] First!
|
||||
[one] {$pos}st
|
||||
[two] {$pos}nd
|
||||
[few] {$pos}rd
|
||||
*[other] {$pos}th
|
||||
}
|
||||
|
||||
|
||||
# Variables in references.
|
||||
-term-using-variable = https://{ $host }
|
||||
message-using-term-with-variable = For example: { -term-using-variable(host: "example.com") }.
|
||||
|
||||
-term-using-variable-2 =
|
||||
{ $case ->
|
||||
*[nominative] Firefox
|
||||
[locative] Firefoksie
|
||||
}
|
||||
message-using-term-with-variable-2-1 = Informacje o { -term-using-variable-2(case: "locative") }.
|
||||
message-using-term-with-variable-2-2 = About { -term-using-variable-2(case: "nominative") }.
|
||||
message-using-term-with-variable-2-default = About { -term-using-variable-2(case: "") }.
|
||||
message-using-term-with-variable-2-not-provided = About { -term-using-variable-2 }.
|
||||
|
||||
-brand-name =
|
||||
{ $case ->
|
||||
*[nominative] Firefox
|
||||
[locative] Firefoksie
|
||||
}
|
||||
|
||||
string-literal = { "string literal" }
|
||||
number-literal-1 = { 1 }
|
||||
number-literal-2 = { -123 }
|
||||
number-literal-3 = { 3.14 }
|
||||
inline-expression-placeable-1 = { { "string literal" } }
|
||||
inline-expression-placeable-2 = { { 123 } }
|
||||
3
dioxus-i18n/examples/data/i18n/en-US.ftl
Normal file
3
dioxus-i18n/examples/data/i18n/en-US.ftl
Normal file
@@ -0,0 +1,3 @@
|
||||
hello_world = Hello, World!
|
||||
|
||||
hello = Hello, {$name}!
|
||||
3
dioxus-i18n/examples/data/i18n/es-ES.ftl
Normal file
3
dioxus-i18n/examples/data/i18n/es-ES.ftl
Normal file
@@ -0,0 +1,3 @@
|
||||
hello_world = Hola, Mundo!
|
||||
|
||||
hello = Hola, {$name}!
|
||||
47
dioxus-i18n/examples/dioxus-desktop.rs
Normal file
47
dioxus-i18n/examples/dioxus-desktop.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_i18n::{prelude::*, t};
|
||||
use unic_langid::langid;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
launch(app);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn Body() -> Element {
|
||||
let mut i18n = i18n();
|
||||
|
||||
let change_to_english = move |_| i18n.set_language(langid!("en-US"));
|
||||
let change_to_spanish = move |_| i18n.set_language(langid!("es-ES"));
|
||||
|
||||
rsx!(
|
||||
button {
|
||||
onclick: change_to_english,
|
||||
label {
|
||||
"English"
|
||||
}
|
||||
}
|
||||
button {
|
||||
onclick: change_to_spanish,
|
||||
label {
|
||||
"Spanish"
|
||||
}
|
||||
}
|
||||
p { { t!("hello_world") } }
|
||||
p { { t!("hello", name: "Dioxus") } }
|
||||
)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
use_init_i18n(|| {
|
||||
I18nConfig::new(langid!("en-US"))
|
||||
.with_locale((langid!("en-US"), include_str!("./data/i18n/en-US.ftl")))
|
||||
.with_locale((
|
||||
langid!("es-ES"),
|
||||
PathBuf::from("./examples/data/i18n/es-ES.ftl"),
|
||||
))
|
||||
});
|
||||
|
||||
rsx!(Body {})
|
||||
}
|
||||
233
dioxus-i18n/examples/fluent-grammar.rs
Normal file
233
dioxus-i18n/examples/fluent-grammar.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
//! This example demonstrates many of the Fluent grammar constructs, and how they are
|
||||
//! used in dioxus-i18n.
|
||||
//! This performs a lookup only, no additional translation files are provided
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_i18n::{prelude::*, tid};
|
||||
use unic_langid::langid;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
launch(app);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[component]
|
||||
fn Body() -> Element {
|
||||
rsx! {
|
||||
table {
|
||||
tbody {
|
||||
tr {
|
||||
td { "Simple message" }
|
||||
td { {tid!("simple-message")} }
|
||||
}
|
||||
tr {
|
||||
td { "Non-existing message: id provided by default when using tid! macro" }
|
||||
td { {tid!("non-existing-message")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with a variable" }
|
||||
td { {tid!("message-with-variable", name: "Value 1")} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("message-with-variable", name: "Value 2")} }
|
||||
}
|
||||
tr {
|
||||
td { "Reference to a term" }
|
||||
td { {tid!("message-referencing-a-term")} }
|
||||
}
|
||||
tr {
|
||||
td { "Use of special characters." }
|
||||
td { {tid!("message-with-special-character")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with blanks." }
|
||||
td { "'" {tid!("blank-is-removed")} "'" }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { "'" {tid!("blank-is-preserved")} "'" }
|
||||
}
|
||||
tr {
|
||||
td { "Message with attributes: root" }
|
||||
td { {tid!("message-with-attributes")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with attributes: attribute" }
|
||||
td { {tid!("message-with-attributes.placeholder")} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("message-with-attributes.aria-label")} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("message-with-attributes.title")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with attributes: not existing" }
|
||||
td { {tid!("message-with-attributes.not-existing")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with attributes: invalid" }
|
||||
td { {tid!("message-with-attributes.placeholder.invalid")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with quotes: cryptic" }
|
||||
td { {tid!("literal-quote-cryptic")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with quotes: preferred" }
|
||||
td { {tid!("literal-quote-preferred")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with Unicode characters: cryptic" }
|
||||
td { {tid!("unicode-cryptic")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with Unicode characters: preferred" }
|
||||
td { {tid!("unicode-preferred")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with a placeable: single-line" }
|
||||
td { {tid!("line-single")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with a placeable: single-line" }
|
||||
td { {tid!("single-line")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with a placeable: multi-line (1)" }
|
||||
td { {tid!("multi-line")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with a placeable: multi-line (2)" }
|
||||
td { pre { {tid!("multi-line")} } }
|
||||
}
|
||||
tr {
|
||||
td { "Message with a placeable: block-line (1)" }
|
||||
td { {tid!("block-line")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message with a placeable: block-line (2)" }
|
||||
td { pre { {tid!("block-line")} } }
|
||||
}
|
||||
tr {
|
||||
td { "Message using functions: no function" }
|
||||
td { pre { {tid!("time-elapsed-no-function", duration: 23.7114812589)} } }
|
||||
}
|
||||
tr {
|
||||
td { "Message using functions: function" }
|
||||
td { pre { {tid!("time-elapsed-function", duration: 23.7114812589)} } }
|
||||
}
|
||||
tr {
|
||||
td { "Reference to a message" }
|
||||
td { {tid!("message-referencing-another-message")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message selection: plurals" }
|
||||
td { {tid!("message-selection-plurals", value: 1)} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("message-selection-plurals", value: 2)} }
|
||||
}
|
||||
tr {
|
||||
td { "Message selection: plurals (default: an 'empty' value must be provided...)" }
|
||||
td { {tid!("message-selection-plurals", value: "")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message selection: plurals (default: ... otherwise an error is raised)" }
|
||||
td { {tid!("message-selection-plurals")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message selection: numeric" }
|
||||
td { {tid!("message-selection-numeric", value: 0.0)} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("message-selection-numeric", value: 0.5)} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("message-selection-numeric", value: 42.0)} }
|
||||
}
|
||||
tr {
|
||||
td { "Message selection: numeric (default)" }
|
||||
td { {tid!("message-selection-numeric", value: "")} }
|
||||
}
|
||||
tr {
|
||||
td { "Message selection: number" }
|
||||
td { {tid!("message-selection-number", pos: 1)} }
|
||||
}
|
||||
tr {
|
||||
td { "" }
|
||||
td { {tid!("message-selection-number", pos: 2)} }
|
||||
}
|
||||
tr {
|
||||
td { "" }
|
||||
td { {tid!("message-selection-number", pos: 3)} }
|
||||
}
|
||||
tr {
|
||||
td { "" }
|
||||
td { {tid!("message-selection-number", pos: 4)} }
|
||||
}
|
||||
tr {
|
||||
td { "Variables in references (1)" }
|
||||
td { {tid!("message-using-term-with-variable")} }
|
||||
}
|
||||
tr {
|
||||
td { "Variables in references (2)" }
|
||||
td { {tid!("message-using-term-with-variable-2-1")} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("message-using-term-with-variable-2-2")} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("message-using-term-with-variable-2-default")} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("message-using-term-with-variable-2-not-provided")} }
|
||||
}
|
||||
tr {
|
||||
td { "Literals: string" }
|
||||
td { {tid!("string-literal")} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("number-literal-1")} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("number-literal-2")} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("number-literal-3")} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("inline-expression-placeable-1")} }
|
||||
}
|
||||
tr {
|
||||
td { }
|
||||
td { {tid!("inline-expression-placeable-2")} }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
use_init_i18n(|| {
|
||||
// Only one example in this path, which contains the complete Fluent grammar.
|
||||
I18nConfig::new(langid!("en")).with_auto_locales(PathBuf::from("./examples/data/fluent/"))
|
||||
});
|
||||
|
||||
rsx!(Body {})
|
||||
}
|
||||
57
dioxus-i18n/examples/freya.rs
Normal file
57
dioxus-i18n/examples/freya.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use dioxus_i18n::{prelude::*, t};
|
||||
use freya::prelude::*;
|
||||
use unic_langid::langid;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
launch_with_props(app, "freya + i18n", (300.0, 200.0));
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn Body() -> Element {
|
||||
let mut i18n = i18n();
|
||||
|
||||
let change_to_english = move |_| i18n.set_language(langid!("en-US"));
|
||||
let change_to_spanish = move |_| i18n.set_language(langid!("es-ES"));
|
||||
|
||||
rsx!(
|
||||
rect {
|
||||
rect {
|
||||
direction: "horizontal",
|
||||
Button {
|
||||
onclick: change_to_english,
|
||||
label {
|
||||
"English"
|
||||
}
|
||||
}
|
||||
Button {
|
||||
onclick: change_to_spanish,
|
||||
label {
|
||||
"Spanish"
|
||||
}
|
||||
}
|
||||
}
|
||||
label { { t!("hello_world") } }
|
||||
label { { t!("hello", name: "Dioxus") } }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
use_init_i18n(|| {
|
||||
I18nConfig::new(langid!("en-US"))
|
||||
.with_locale((langid!("en-US"), include_str!("./data/i18n/en-US.ftl")))
|
||||
.with_locale((
|
||||
langid!("es-ES"),
|
||||
PathBuf::from("./examples/data/i18n/es-ES.ftl"),
|
||||
))
|
||||
});
|
||||
|
||||
rsx!(Body {})
|
||||
}
|
||||
31
dioxus-i18n/src/error.rs
Normal file
31
dioxus-i18n/src/error.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("invalid message id: '{0}'")]
|
||||
InvalidMessageId(String),
|
||||
|
||||
#[error("message id not found for key: '{0}'")]
|
||||
MessageIdNotFound(String),
|
||||
|
||||
#[error("attribute id not found for key: '{0}'")]
|
||||
AttributeIdNotFound(String),
|
||||
|
||||
#[error("message pattern not found for key: '{0}'")]
|
||||
MessagePatternNotFound(String),
|
||||
|
||||
#[error("fluent errors during lookup:\n{0}")]
|
||||
FluentErrorsDetected(String),
|
||||
|
||||
#[error("failed to read locale resource from path: {0}")]
|
||||
LocaleResourcePathReadFailed(String),
|
||||
|
||||
#[error("fallback for \"{0}\" must have locale")]
|
||||
FallbackMustHaveLocale(String),
|
||||
|
||||
#[error("language id cannot be determined - reason: {0}")]
|
||||
InvalidLanguageId(String),
|
||||
|
||||
#[error("invalid path: {0}")]
|
||||
InvalidPath(String),
|
||||
}
|
||||
101
dioxus-i18n/src/i18n_macro.rs
Normal file
101
dioxus-i18n/src/i18n_macro.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
//! Key translation macros.
|
||||
//!
|
||||
//! Using file:
|
||||
//!
|
||||
//! ```ftl
|
||||
//! # en-US.ftl
|
||||
//! #
|
||||
//! hello = Hello, {$name}!
|
||||
//! ```
|
||||
|
||||
/// Translate message from key, returning [`crate::prelude::DioxusI18nError`] if id not found...
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # use dioxus_i18n::{te, prelude::*};
|
||||
/// # use unic_langid::langid;
|
||||
/// # #[component]
|
||||
/// # fn Example() -> Element {
|
||||
/// # let lang = langid!("en-US");
|
||||
/// # let config = I18nConfig::new(lang.clone()).with_locale((lang.clone(), "hello = Hello, {$name}")).with_fallback(lang.clone());
|
||||
/// # let mut i18n = use_init_i18n(|| config);
|
||||
/// let name = "Avery Gigglesworth";
|
||||
/// let hi = te!("hello", name: {name}).expect("message id 'name' should be present");
|
||||
/// assert_eq!(hi, "Hello, Avery Gigglesworth");
|
||||
/// # rsx! { "" }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
#[macro_export]
|
||||
macro_rules! te {
|
||||
($id:expr, $( $name:ident : $value:expr ),* ) => {
|
||||
{
|
||||
let mut params_map = $crate::fluent::FluentArgs::new();
|
||||
$(
|
||||
params_map.set(stringify!($name), $value);
|
||||
)*
|
||||
$crate::prelude::i18n().try_translate_with_args($id, Some(¶ms_map))
|
||||
}
|
||||
};
|
||||
|
||||
($id:expr ) => {{
|
||||
$crate::prelude::i18n().try_translate($id)
|
||||
}};
|
||||
}
|
||||
|
||||
/// Translate message from key, panic! if id not found...
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # use dioxus_i18n::{t, prelude::*};
|
||||
/// # use unic_langid::langid;
|
||||
/// # #[component]
|
||||
/// # fn Example() -> Element {
|
||||
/// # let lang = langid!("en-US");
|
||||
/// # let config = I18nConfig::new(lang.clone()).with_locale((lang.clone(), "hello = Hello, {$name}")).with_fallback(lang.clone());
|
||||
/// # let mut i18n = use_init_i18n(|| config);
|
||||
/// let name = "Avery Gigglesworth";
|
||||
/// let hi = t!("hello", name: {name});
|
||||
/// assert_eq!(hi, "Hello, Avery Gigglesworth");
|
||||
/// # rsx! { "" }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
#[macro_export]
|
||||
macro_rules! t {
|
||||
($id:expr, $( $name:ident : $value:expr ),* ) => {
|
||||
$crate::te!($id, $( $name : $value ),*).unwrap_or_else(|e| panic!("{}", e.to_string()))
|
||||
};
|
||||
|
||||
($id:expr ) => {{
|
||||
$crate::te!($id).unwrap_or_else(|e| panic!("{}", e.to_string()))
|
||||
}};
|
||||
}
|
||||
|
||||
/// Translate message from key, return id if no translation found...
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # use dioxus_i18n::{tid, prelude::*};
|
||||
/// # use unic_langid::langid;
|
||||
/// # #[component]
|
||||
/// # fn Example() -> Element {
|
||||
/// # let lang = langid!("en-US");
|
||||
/// # let config = I18nConfig::new(lang.clone()).with_locale((lang.clone(), "hello = Hello, {$name}")).with_fallback(lang.clone());
|
||||
/// # let mut i18n = use_init_i18n(|| config);
|
||||
/// let message = tid!("no-key");
|
||||
/// assert_eq!(message, "message-id: no-key should be translated");
|
||||
/// # rsx! { "" }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
#[macro_export]
|
||||
macro_rules! tid {
|
||||
($id:expr, $( $name:ident : $value:expr ),* ) => {
|
||||
$crate::te!($id, $( $name : $value ),*).unwrap_or_else(|e| e.to_string())
|
||||
};
|
||||
|
||||
($id:expr ) => {{
|
||||
$crate::te!($id).unwrap_or_else(|e| e.to_string())
|
||||
}};
|
||||
}
|
||||
12
dioxus-i18n/src/lib.rs
Normal file
12
dioxus-i18n/src/lib.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
mod error;
|
||||
pub mod i18n_macro;
|
||||
pub mod use_i18n;
|
||||
|
||||
pub use fluent;
|
||||
pub use unic_langid;
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::error::Error as DioxusI18nError;
|
||||
pub use crate::use_i18n::*;
|
||||
}
|
||||
703
dioxus-i18n/src/use_i18n.rs
Normal file
703
dioxus-i18n/src/use_i18n.rs
Normal file
@@ -0,0 +1,703 @@
|
||||
use super::error::Error;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use fluent::{FluentArgs, FluentBundle, FluentResource};
|
||||
use unic_langid::LanguageIdentifier;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// `Locale` is a "place-holder" around what will eventually be a `fluent::FluentBundle`
|
||||
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||
pub struct Locale {
|
||||
id: LanguageIdentifier,
|
||||
resource: LocaleResource,
|
||||
}
|
||||
|
||||
impl Locale {
|
||||
pub fn new_static(id: LanguageIdentifier, str: &'static str) -> Self {
|
||||
Self {
|
||||
id,
|
||||
resource: LocaleResource::Static(str),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn new_dynamic(id: LanguageIdentifier, path: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
resource: LocaleResource::Path(path.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<(LanguageIdentifier, T)> for Locale
|
||||
where
|
||||
T: Into<LocaleResource>,
|
||||
{
|
||||
fn from((id, resource): (LanguageIdentifier, T)) -> Self {
|
||||
let resource = resource.into();
|
||||
Self { id, resource }
|
||||
}
|
||||
}
|
||||
|
||||
/// A `LocaleResource` can be static text, or a filesystem file (not supported in WASM).
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum LocaleResource {
|
||||
Static(&'static str),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
impl LocaleResource {
|
||||
pub fn try_to_resource_string(&self) -> Result<String, Error> {
|
||||
match self {
|
||||
Self::Static(str) => Ok(str.to_string()),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Self::Path(path) => std::fs::read_to_string(path)
|
||||
.map_err(|e| Error::LocaleResourcePathReadFailed(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_resource_string(&self) -> String {
|
||||
let result = self.try_to_resource_string();
|
||||
match result {
|
||||
Ok(string) => string,
|
||||
Err(err) => panic!("failed to create resource string {:?}: {}", self, err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for LocaleResource {
|
||||
fn from(value: &'static str) -> Self {
|
||||
Self::Static(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl From<PathBuf> for LocaleResource {
|
||||
fn from(value: PathBuf) -> Self {
|
||||
Self::Path(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// The configuration for `I18n`.
|
||||
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||
pub struct I18nConfig {
|
||||
/// The initial language, can be later changed with [`I18n::set_language`]
|
||||
id: LanguageIdentifier,
|
||||
|
||||
/// The final fallback language if no other locales are found for `id`.
|
||||
/// A `Locale` must exist in `locales' if `fallback` is defined.
|
||||
fallback: Option<LanguageIdentifier>,
|
||||
|
||||
/// The locale_resources added to the configuration.
|
||||
locale_resources: Vec<LocaleResource>,
|
||||
|
||||
/// The locales added to the configuration.
|
||||
locales: HashMap<LanguageIdentifier, usize>,
|
||||
}
|
||||
|
||||
impl I18nConfig {
|
||||
/// Create an i18n config with the selected [LanguageIdentifier].
|
||||
pub fn new(id: LanguageIdentifier) -> Self {
|
||||
Self {
|
||||
id,
|
||||
fallback: None,
|
||||
locale_resources: Vec::new(),
|
||||
locales: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a fallback [LanguageIdentifier].
|
||||
pub fn with_fallback(mut self, fallback: LanguageIdentifier) -> Self {
|
||||
self.fallback = Some(fallback);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add [Locale].
|
||||
/// It is possible to share locales resources. If this locale's resource
|
||||
/// matches a previously added one, then this locale will use the existing one.
|
||||
/// This is primarily for the static locale_resources to avoid string duplication.
|
||||
pub fn with_locale<T>(mut self, locale: T) -> Self
|
||||
where
|
||||
T: Into<Locale>,
|
||||
{
|
||||
let locale = locale.into();
|
||||
let locale_resources_len = self.locale_resources.len();
|
||||
|
||||
let index = self
|
||||
.locale_resources
|
||||
.iter()
|
||||
.position(|r| *r == locale.resource)
|
||||
.unwrap_or(locale_resources_len);
|
||||
|
||||
if index == locale_resources_len {
|
||||
self.locale_resources.push(locale.resource)
|
||||
};
|
||||
|
||||
self.locales.insert(locale.id, index);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple locales from given folder, based on their filename.
|
||||
///
|
||||
/// If the path represents a folder, then the folder will be deep traversed for
|
||||
/// all '*.ftl' files. If the filename represents a [LanguageIdentifier] then it
|
||||
/// will be added to the config.
|
||||
///
|
||||
/// If the path represents a file, then the filename must represent a
|
||||
/// unic_langid::LanguageIdentifier for it to be added to the config.
|
||||
///
|
||||
/// The method is not available for `wasm32` builds.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn try_with_auto_locales(self, path: PathBuf) -> Result<Self, Error> {
|
||||
if path.is_dir() {
|
||||
let files = find_ftl_files(&path)?;
|
||||
files
|
||||
.into_iter()
|
||||
.try_fold(self, |acc, file| acc.with_auto_pathbuf(file))
|
||||
} else if is_ftl_file(&path) {
|
||||
self.with_auto_pathbuf(path)
|
||||
} else {
|
||||
Err(Error::InvalidPath(path.to_string_lossy().to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn with_auto_pathbuf(self, file: PathBuf) -> Result<Self, Error> {
|
||||
assert!(is_ftl_file(&file));
|
||||
|
||||
let stem = file.file_stem().ok_or_else(|| {
|
||||
Error::InvalidLanguageId(format!("No file stem: '{}'", file.display()))
|
||||
})?;
|
||||
|
||||
let id_str = stem.to_str().ok_or_else(|| {
|
||||
Error::InvalidLanguageId(format!("Cannot convert: {}", stem.to_string_lossy()))
|
||||
})?;
|
||||
|
||||
let id = LanguageIdentifier::from_bytes(id_str.as_bytes())
|
||||
.map_err(|e| Error::InvalidLanguageId(e.to_string()))?;
|
||||
|
||||
Ok(self.with_locale((id, file)))
|
||||
}
|
||||
|
||||
/// Add multiple locales from given folder, based on their filename.
|
||||
///
|
||||
/// Will panic! on error.
|
||||
///
|
||||
/// The method is not available for `wasm32` builds.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn with_auto_locales(self, path: PathBuf) -> Self {
|
||||
let path_name = path.display().to_string();
|
||||
let result = self.try_with_auto_locales(path);
|
||||
match result {
|
||||
Ok(result) => result,
|
||||
Err(err) => panic!(
|
||||
"with_auto_locales must have valid pathbuf {}: {}",
|
||||
path_name, err
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn find_ftl_files(folder: &PathBuf) -> Result<Vec<PathBuf>, Error> {
|
||||
let ftl_files: Vec<PathBuf> = WalkDir::new(folder)
|
||||
.into_iter()
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| is_ftl_file(entry.path()))
|
||||
.map(|entry| entry.path().to_path_buf())
|
||||
.collect();
|
||||
|
||||
Ok(ftl_files)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn is_ftl_file(entry: &Path) -> bool {
|
||||
entry.is_file() && entry.extension().map(|ext| ext == "ftl").unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Initialize an i18n provider.
|
||||
pub fn try_use_init_i18n(init: impl FnOnce() -> I18nConfig) -> Result<I18n, Error> {
|
||||
use_context_provider(move || {
|
||||
// Coverage false -ve: See https://github.com/xd009642/tarpaulin/issues/1675
|
||||
let I18nConfig {
|
||||
id,
|
||||
fallback,
|
||||
locale_resources,
|
||||
locales,
|
||||
} = init();
|
||||
|
||||
I18n::try_new(id, fallback, locale_resources, locales)
|
||||
})
|
||||
}
|
||||
|
||||
/// Initialize an i18n provider.
|
||||
pub fn use_init_i18n(init: impl FnOnce() -> I18nConfig) -> I18n {
|
||||
use_context_provider(move || {
|
||||
// Coverage false -ve: See https://github.com/xd009642/tarpaulin/issues/1675
|
||||
let I18nConfig {
|
||||
id,
|
||||
fallback,
|
||||
locale_resources,
|
||||
locales,
|
||||
} = init();
|
||||
|
||||
match I18n::try_new(id, fallback, locale_resources, locales) {
|
||||
Ok(i18n) => i18n,
|
||||
Err(e) => panic!("Failed to create I18n context: {}", e),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct I18n {
|
||||
selected_language: Signal<LanguageIdentifier>,
|
||||
fallback_language: Signal<Option<LanguageIdentifier>>,
|
||||
locale_resources: Signal<Vec<LocaleResource>>,
|
||||
locales: Signal<HashMap<LanguageIdentifier, usize>>,
|
||||
active_bundle: Signal<FluentBundle<FluentResource>>,
|
||||
}
|
||||
|
||||
impl I18n {
|
||||
pub fn try_new(
|
||||
selected_language: LanguageIdentifier,
|
||||
fallback_language: Option<LanguageIdentifier>,
|
||||
locale_resources: Vec<LocaleResource>,
|
||||
locales: HashMap<LanguageIdentifier, usize>,
|
||||
) -> Result<Self, Error> {
|
||||
let bundle = try_create_bundle(
|
||||
&selected_language,
|
||||
&fallback_language,
|
||||
&locale_resources,
|
||||
&locales,
|
||||
)?;
|
||||
Ok(Self {
|
||||
selected_language: Signal::new(selected_language),
|
||||
fallback_language: Signal::new(fallback_language),
|
||||
locale_resources: Signal::new(locale_resources),
|
||||
locales: Signal::new(locales),
|
||||
active_bundle: Signal::new(bundle),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
selected_language: LanguageIdentifier,
|
||||
fallback_language: Option<LanguageIdentifier>,
|
||||
locale_resources: Vec<LocaleResource>,
|
||||
locales: HashMap<LanguageIdentifier, usize>,
|
||||
) -> Self {
|
||||
let result = Self::try_new(
|
||||
selected_language,
|
||||
fallback_language,
|
||||
locale_resources,
|
||||
locales,
|
||||
);
|
||||
match result {
|
||||
Ok(i18n) => i18n,
|
||||
Err(err) => panic!("I18n cannot be created: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_translate_with_args(
|
||||
&self,
|
||||
msg: &str,
|
||||
args: Option<&FluentArgs>,
|
||||
) -> Result<String, Error> {
|
||||
let (message_id, attribute_name) = Self::decompose_identifier(msg)?;
|
||||
|
||||
let bundle = self.active_bundle.read();
|
||||
|
||||
let message = bundle
|
||||
.get_message(message_id)
|
||||
.ok_or_else(|| Error::MessageIdNotFound(message_id.into()))?;
|
||||
|
||||
let pattern = if let Some(attribute_name) = attribute_name {
|
||||
let attribute = message
|
||||
.get_attribute(attribute_name)
|
||||
.ok_or_else(|| Error::AttributeIdNotFound(msg.to_string()))?;
|
||||
attribute.value()
|
||||
} else {
|
||||
message
|
||||
.value()
|
||||
.ok_or_else(|| Error::MessagePatternNotFound(message_id.into()))?
|
||||
};
|
||||
|
||||
let mut errors = vec![];
|
||||
let translation = bundle
|
||||
.format_pattern(pattern, args, &mut errors)
|
||||
.to_string();
|
||||
|
||||
(errors.is_empty())
|
||||
.then_some(translation)
|
||||
.ok_or_else(|| Error::FluentErrorsDetected(format!("{:#?}", errors)))
|
||||
}
|
||||
|
||||
pub fn decompose_identifier(msg: &str) -> Result<(&str, Option<&str>), Error> {
|
||||
let parts: Vec<&str> = msg.split('.').collect();
|
||||
match parts.as_slice() {
|
||||
[message_id] => Ok((message_id, None)),
|
||||
[message_id, attribute_name] => Ok((message_id, Some(attribute_name))),
|
||||
_ => Err(Error::InvalidMessageId(msg.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn translate_with_args(&self, msg: &str, args: Option<&FluentArgs>) -> String {
|
||||
let result = self.try_translate_with_args(msg, args);
|
||||
match result {
|
||||
Ok(translation) => translation,
|
||||
Err(err) => panic!("Failed to translate {}: {}", msg, err),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn try_translate(&self, msg: &str) -> Result<String, Error> {
|
||||
self.try_translate_with_args(msg, None)
|
||||
}
|
||||
|
||||
pub fn translate(&self, msg: &str) -> String {
|
||||
let result = self.try_translate(msg);
|
||||
match result {
|
||||
Ok(translation) => translation,
|
||||
Err(err) => panic!("Failed to translate {}: {}", msg, err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the selected language.
|
||||
#[inline]
|
||||
pub fn language(&self) -> LanguageIdentifier {
|
||||
self.selected_language.read().clone()
|
||||
}
|
||||
|
||||
/// Get the fallback language.
|
||||
pub fn fallback_language(&self) -> Option<LanguageIdentifier> {
|
||||
self.fallback_language.read().clone()
|
||||
}
|
||||
|
||||
/// Update the selected language.
|
||||
pub fn try_set_language(&mut self, id: LanguageIdentifier) -> Result<(), Error> {
|
||||
*self.selected_language.write() = id;
|
||||
self.try_update_active_bundle()
|
||||
}
|
||||
|
||||
/// Update the selected language.
|
||||
pub fn set_language(&mut self, id: LanguageIdentifier) {
|
||||
let id_name = id.to_string();
|
||||
let result = self.try_set_language(id);
|
||||
match result {
|
||||
Ok(()) => (),
|
||||
Err(err) => panic!("cannot set language {}: {}", id_name, err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the fallback language.
|
||||
pub fn try_set_fallback_language(&mut self, id: LanguageIdentifier) -> Result<(), Error> {
|
||||
self.locales
|
||||
.read()
|
||||
.get(&id)
|
||||
.ok_or_else(|| Error::FallbackMustHaveLocale(id.to_string()))?;
|
||||
|
||||
*self.fallback_language.write() = Some(id);
|
||||
self.try_update_active_bundle()
|
||||
}
|
||||
|
||||
/// Update the fallback language.
|
||||
pub fn set_fallback_language(&mut self, id: LanguageIdentifier) {
|
||||
let id_name = id.to_string();
|
||||
let result = self.try_set_fallback_language(id);
|
||||
match result {
|
||||
Ok(()) => (),
|
||||
Err(err) => panic!("cannot set fallback language {}: {}", id_name, err),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_update_active_bundle(&mut self) -> Result<(), Error> {
|
||||
let bundle = try_create_bundle(
|
||||
&self.selected_language.peek(),
|
||||
&self.fallback_language.peek(),
|
||||
&self.locale_resources.peek(),
|
||||
&self.locales.peek(),
|
||||
)?;
|
||||
|
||||
self.active_bundle.set(bundle);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn try_create_bundle(
|
||||
selected_language: &LanguageIdentifier,
|
||||
fallback_language: &Option<LanguageIdentifier>,
|
||||
locale_resources: &[LocaleResource],
|
||||
locales: &HashMap<LanguageIdentifier, usize>,
|
||||
) -> Result<FluentBundle<FluentResource>, Error> {
|
||||
let add_resource = move |bundle: &mut FluentBundle<FluentResource>,
|
||||
langid: &LanguageIdentifier,
|
||||
locale_resources: &[LocaleResource]| {
|
||||
if let Some(&i) = locales.get(langid) {
|
||||
let resource = &locale_resources[i];
|
||||
let resource =
|
||||
FluentResource::try_new(resource.try_to_resource_string()?).map_err(|e| {
|
||||
Error::FluentErrorsDetected(format!("resource langid: {}\n{:#?}", langid, e))
|
||||
})?;
|
||||
bundle.add_resource_overriding(resource);
|
||||
};
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let mut bundle = FluentBundle::new(vec![selected_language.clone()]);
|
||||
if let Some(fallback_language) = fallback_language {
|
||||
add_resource(&mut bundle, fallback_language, locale_resources)?;
|
||||
}
|
||||
|
||||
let (language, script, region, variants) = selected_language.clone().into_parts();
|
||||
let variants_lang = LanguageIdentifier::from_parts(language, script, region, &variants);
|
||||
let region_lang = LanguageIdentifier::from_parts(language, script, region, &[]);
|
||||
let script_lang = LanguageIdentifier::from_parts(language, script, None, &[]);
|
||||
let language_lang = LanguageIdentifier::from_parts(language, None, None, &[]);
|
||||
|
||||
add_resource(&mut bundle, &language_lang, locale_resources)?;
|
||||
add_resource(&mut bundle, &script_lang, locale_resources)?;
|
||||
add_resource(&mut bundle, ®ion_lang, locale_resources)?;
|
||||
add_resource(&mut bundle, &variants_lang, locale_resources)?;
|
||||
|
||||
/* Add this code when the fluent crate includes FluentBundle::add_builtins.
|
||||
* This will allow the use of built-in functions like `NUMBER` and `DATETIME`.
|
||||
* See [Fluent issue](https://github.com/projectfluent/fluent-rs/issues/181) for more information.
|
||||
bundle
|
||||
.add_builtins()
|
||||
.map_err(|e| Error::FluentErrorsDetected(e.to_string()))?;
|
||||
*/
|
||||
|
||||
Ok(bundle)
|
||||
}
|
||||
|
||||
pub fn i18n() -> I18n {
|
||||
consume_context()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use unic_langid::langid;
|
||||
|
||||
#[test]
|
||||
fn can_add_locale_to_config_explicit_locale() {
|
||||
const LANG_A: LanguageIdentifier = langid!("la-LA");
|
||||
const LANG_B: LanguageIdentifier = langid!("la-LB");
|
||||
const LANG_C: LanguageIdentifier = langid!("la-LC");
|
||||
|
||||
let config = I18nConfig::new(LANG_A)
|
||||
.with_locale(Locale::new_static(LANG_B, "lang = lang_b"))
|
||||
.with_locale(Locale::new_dynamic(LANG_C, PathBuf::new()));
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
I18nConfig {
|
||||
id: LANG_A,
|
||||
fallback: None,
|
||||
locale_resources: vec![
|
||||
LocaleResource::Static("lang = lang_b"),
|
||||
LocaleResource::Path(PathBuf::new()),
|
||||
],
|
||||
locales: HashMap::from([(LANG_B, 0), (LANG_C, 1)]),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_add_locale_to_config_implicit_locale() {
|
||||
const LANG_A: LanguageIdentifier = langid!("la-LA");
|
||||
const LANG_B: LanguageIdentifier = langid!("la-LB");
|
||||
const LANG_C: LanguageIdentifier = langid!("la-LC");
|
||||
|
||||
let config = I18nConfig::new(LANG_A)
|
||||
.with_locale((LANG_B, "lang = lang_b"))
|
||||
.with_locale((LANG_C, PathBuf::new()));
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
I18nConfig {
|
||||
id: LANG_A,
|
||||
fallback: None,
|
||||
locale_resources: vec![
|
||||
LocaleResource::Static("lang = lang_b"),
|
||||
LocaleResource::Path(PathBuf::new())
|
||||
],
|
||||
locales: HashMap::from([(LANG_B, 0), (LANG_C, 1)]),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_add_locale_string_to_config() {
|
||||
const LANG_A: LanguageIdentifier = langid!("la-LA");
|
||||
const LANG_B: LanguageIdentifier = langid!("la-LB");
|
||||
|
||||
let config = I18nConfig::new(LANG_A).with_locale((LANG_B, "lang = lang_b"));
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
I18nConfig {
|
||||
id: LANG_A,
|
||||
fallback: None,
|
||||
locale_resources: vec![LocaleResource::Static("lang = lang_b")],
|
||||
locales: HashMap::from([(LANG_B, 0)]),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_add_shared_locale_string_to_config() {
|
||||
const LANG_A: LanguageIdentifier = langid!("la-LA");
|
||||
const LANG_B: LanguageIdentifier = langid!("la-LB");
|
||||
const LANG_C: LanguageIdentifier = langid!("la-LC");
|
||||
|
||||
let shared_string = "lang = a language";
|
||||
let config = I18nConfig::new(LANG_A)
|
||||
.with_locale((LANG_B, shared_string))
|
||||
.with_locale((LANG_C, shared_string));
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
I18nConfig {
|
||||
id: LANG_A,
|
||||
fallback: None,
|
||||
locale_resources: vec![LocaleResource::Static(shared_string)],
|
||||
locales: HashMap::from([(LANG_B, 0), (LANG_C, 0)]),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_add_locale_pathbuf_to_config() {
|
||||
const LANG_A: LanguageIdentifier = langid!("la-LA");
|
||||
const LANG_C: LanguageIdentifier = langid!("la-LC");
|
||||
|
||||
let config = I18nConfig::new(LANG_A)
|
||||
.with_locale((LANG_C, PathBuf::from("./test/data/fallback/la.ftl")));
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
I18nConfig {
|
||||
id: LANG_A,
|
||||
fallback: None,
|
||||
locale_resources: vec![LocaleResource::Path(PathBuf::from(
|
||||
"./test/data/fallback/la.ftl"
|
||||
))],
|
||||
locales: HashMap::from([(LANG_C, 0)]),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_add_shared_locale_pathbuf_to_config() {
|
||||
const LANG_A: LanguageIdentifier = langid!("la-LA");
|
||||
const LANG_B: LanguageIdentifier = langid!("la-LB");
|
||||
const LANG_C: LanguageIdentifier = langid!("la-LC");
|
||||
|
||||
let shared_pathbuf = PathBuf::from("./test/data/fallback/la.ftl");
|
||||
|
||||
let config = I18nConfig::new(LANG_A)
|
||||
.with_locale((LANG_B, shared_pathbuf.clone()))
|
||||
.with_locale((LANG_C, shared_pathbuf.clone()));
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
I18nConfig {
|
||||
id: LANG_A,
|
||||
fallback: None,
|
||||
locale_resources: vec![LocaleResource::Path(shared_pathbuf)],
|
||||
locales: HashMap::from([(LANG_B, 0), (LANG_C, 0)]),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_auto_add_locales_folder_to_config() {
|
||||
const LANG_A: LanguageIdentifier = langid!("la-LA");
|
||||
|
||||
let root_path_str = &format!("{}/tests/data/fallback/", env!("CARGO_MANIFEST_DIR"));
|
||||
let pathbuf = PathBuf::from(root_path_str);
|
||||
|
||||
let config = I18nConfig::new(LANG_A)
|
||||
.try_with_auto_locales(pathbuf)
|
||||
.ok()
|
||||
.unwrap();
|
||||
|
||||
let expected_locales = [
|
||||
"fb-FB",
|
||||
"la",
|
||||
"la-Scpt",
|
||||
"la-Scpt-LA",
|
||||
"la-Scpt-LA-variants",
|
||||
];
|
||||
|
||||
assert_eq!(config.locales.len(), expected_locales.len());
|
||||
assert_eq!(config.locale_resources.len(), expected_locales.len());
|
||||
|
||||
expected_locales.into_iter().for_each(|l| {
|
||||
let expected_filename = format!("{root_path_str}/{l}.ftl");
|
||||
let id = LanguageIdentifier::from_bytes(l.as_bytes()).unwrap();
|
||||
assert!(config.locales.get(&id).is_some());
|
||||
assert!(config
|
||||
.locale_resources
|
||||
.contains(&LocaleResource::Path(PathBuf::from(expected_filename))));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_auto_add_locales_file_to_config() {
|
||||
const LANG_A: LanguageIdentifier = langid!("la-LA");
|
||||
|
||||
let path_str = &format!(
|
||||
"{}/tests/data/fallback/fb-FB.ftl",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
);
|
||||
let pathbuf = PathBuf::from(path_str);
|
||||
|
||||
let config = I18nConfig::new(LANG_A)
|
||||
.try_with_auto_locales(pathbuf.clone())
|
||||
.ok()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.locales.len(), 1);
|
||||
assert!(config.locales.get(&langid!("fb-FB")).is_some());
|
||||
|
||||
assert_eq!(config.locale_resources.len(), 1);
|
||||
assert!(config
|
||||
.locale_resources
|
||||
.contains(&LocaleResource::Path(pathbuf)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn will_fail_auto_locales_with_invalid_folder() {
|
||||
const LANG_A: LanguageIdentifier = langid!("la-LA");
|
||||
|
||||
let root_path_str = &format!("{}/non_existing_path/", env!("CARGO_MANIFEST_DIR"));
|
||||
let pathbuf = PathBuf::from(root_path_str);
|
||||
|
||||
let config = I18nConfig::new(LANG_A).try_with_auto_locales(pathbuf);
|
||||
assert_eq!(config.is_err(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn will_fail_auto_locales_with_invalid_file() {
|
||||
const LANG_A: LanguageIdentifier = langid!("la-LA");
|
||||
|
||||
let path_str = &format!(
|
||||
"{}/tests/data/fallback/invalid_language_id.ftl",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
);
|
||||
let pathbuf = PathBuf::from(path_str);
|
||||
|
||||
let config = I18nConfig::new(LANG_A).try_with_auto_locales(pathbuf);
|
||||
assert_eq!(config.is_err(), true);
|
||||
}
|
||||
}
|
||||
13
dioxus-i18n/tests/README.md
Normal file
13
dioxus-i18n/tests/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Note
|
||||
|
||||
//*****************************************************************************
|
||||
//
|
||||
// This set of tests takes a heavy handed approach to errors, whereby the
|
||||
// process is exited. This is done because panic! and assert_eq! failures
|
||||
// are trapped within `dioxus::runtime::RuntimeGuard`.
|
||||
// Unfortunately panic! is still made silent.
|
||||
//
|
||||
// Errors will be shown with:
|
||||
// cargo test -- --nocapture
|
||||
//
|
||||
//*****************************************************************************
|
||||
3
dioxus-i18n/tests/common/mod.rs
Normal file
3
dioxus-i18n/tests/common/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod test_hook;
|
||||
|
||||
pub(crate) use test_hook::test_hook;
|
||||
85
dioxus-i18n/tests/common/test_hook.rs
Normal file
85
dioxus-i18n/tests/common/test_hook.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
// Lifted from: https://dioxuslabs.com/learn/0.6/cookbook/testing
|
||||
//
|
||||
// Much curtialed functionality and massaged to use in the local testing
|
||||
// here. This hook isn't intended for reuse.
|
||||
//
|
||||
|
||||
use dioxus::{dioxus_core::NoOpMutations, prelude::*};
|
||||
use futures::FutureExt;
|
||||
|
||||
use std::{cell::RefCell, fmt::Debug, rc::Rc};
|
||||
|
||||
pub(crate) fn test_hook<V: 'static>(
|
||||
initialize: impl FnMut() -> V + 'static,
|
||||
check: impl FnMut(V, &mut Assertions) + 'static,
|
||||
) {
|
||||
#[derive(Props)]
|
||||
struct MockAppComponent<I: 'static, C: 'static> {
|
||||
hook: Rc<RefCell<I>>,
|
||||
check: Rc<RefCell<C>>,
|
||||
}
|
||||
|
||||
impl<I, C> PartialEq for MockAppComponent<I, C> {
|
||||
fn eq(&self, _: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, C> Clone for MockAppComponent<I, C> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
hook: self.hook.clone(),
|
||||
check: self.check.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mock_app<I: FnMut() -> V, C: FnMut(V, &mut Assertions), V>(
|
||||
props: MockAppComponent<I, C>,
|
||||
) -> Element {
|
||||
let value = props.hook.borrow_mut()();
|
||||
|
||||
let mut assertions = Assertions::new();
|
||||
|
||||
props.check.borrow_mut()(value, &mut assertions);
|
||||
|
||||
rsx! { div {} }
|
||||
}
|
||||
|
||||
let mut vdom = VirtualDom::new_with_props(
|
||||
mock_app,
|
||||
MockAppComponent {
|
||||
hook: Rc::new(RefCell::new(initialize)),
|
||||
check: Rc::new(RefCell::new(check)),
|
||||
},
|
||||
);
|
||||
|
||||
vdom.rebuild_in_place();
|
||||
|
||||
while vdom.wait_for_work().now_or_never().is_some() {
|
||||
vdom.render_immediate(&mut NoOpMutations);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Assertions {}
|
||||
|
||||
impl Assertions {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
pub fn assert<T>(&mut self, actual: T, expected: T, id: &str)
|
||||
where
|
||||
T: PartialEq + Debug,
|
||||
{
|
||||
if actual != expected {
|
||||
eprintln!(
|
||||
"***** ERROR in {}: actual: '{:?}' != expected: '{:?}' *****\n",
|
||||
id, actual, expected
|
||||
);
|
||||
std::process::exit(-1);
|
||||
};
|
||||
}
|
||||
}
|
||||
5
dioxus-i18n/tests/data/fallback/fb-FB.ftl
Normal file
5
dioxus-i18n/tests/data/fallback/fb-FB.ftl
Normal file
@@ -0,0 +1,5 @@
|
||||
fallback = fallback only
|
||||
language = fallback language
|
||||
script = fallback script
|
||||
region = fallback region
|
||||
variants = fallback variants
|
||||
1
dioxus-i18n/tests/data/fallback/la-Scpt-LA-variants.ftl
Normal file
1
dioxus-i18n/tests/data/fallback/la-Scpt-LA-variants.ftl
Normal file
@@ -0,0 +1 @@
|
||||
variants = variants only
|
||||
2
dioxus-i18n/tests/data/fallback/la-Scpt-LA.ftl
Normal file
2
dioxus-i18n/tests/data/fallback/la-Scpt-LA.ftl
Normal file
@@ -0,0 +1,2 @@
|
||||
region = region only
|
||||
variants = region variants
|
||||
3
dioxus-i18n/tests/data/fallback/la-Scpt.ftl
Normal file
3
dioxus-i18n/tests/data/fallback/la-Scpt.ftl
Normal file
@@ -0,0 +1,3 @@
|
||||
script = script only
|
||||
region = script region
|
||||
variants = script variants
|
||||
4
dioxus-i18n/tests/data/fallback/la.ftl
Normal file
4
dioxus-i18n/tests/data/fallback/la.ftl
Normal file
@@ -0,0 +1,4 @@
|
||||
language = language only
|
||||
script = language script
|
||||
region = language region
|
||||
variants = language variants
|
||||
5
dioxus-i18n/tests/data/i18n/en.ftl
Normal file
5
dioxus-i18n/tests/data/i18n/en.ftl
Normal file
@@ -0,0 +1,5 @@
|
||||
hello = Hello, {$name}!
|
||||
simple = Hello, Zaphod!
|
||||
my_component = My Component
|
||||
.placeholder = Component's placeholder
|
||||
.hint = Component's hint with parameter {$name}
|
||||
44
dioxus-i18n/tests/defects_spec.rs
Normal file
44
dioxus-i18n/tests/defects_spec.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
|
||||
use dioxus_i18n::{
|
||||
prelude::{use_init_i18n, I18n, I18nConfig},
|
||||
t,
|
||||
};
|
||||
use unic_langid::{langid, LanguageIdentifier};
|
||||
|
||||
#[test]
|
||||
fn issue_15_recent_change_to_t_macro_unnecessarily_breaks_v0_3_code_test_attr() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| {
|
||||
let name = "World";
|
||||
t!(&format!("hello"), name: name)
|
||||
});
|
||||
proxy.assert(panic.is_ok(), true, "translate_from_static_source");
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"Hello, \u{2068}World\u{2069}!".to_string(),
|
||||
"translate_from_static_source",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_15_recent_change_to_t_macro_unnecessarily_breaks_v0_3_code_test_no_attr() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| t!(&format!("simple")));
|
||||
proxy.assert(panic.is_ok(), true, "translate_from_static_source");
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"Hello, Zaphod!".to_string(),
|
||||
"translate_from_static_source",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const EN: LanguageIdentifier = langid!("en");
|
||||
|
||||
fn i18n_from_static() -> I18n {
|
||||
let config = I18nConfig::new(EN).with_locale((EN, include_str!("./data/i18n/en.ftl")));
|
||||
use_init_i18n(|| config)
|
||||
}
|
||||
99
dioxus-i18n/tests/graceful_fallback_spec.rs
Normal file
99
dioxus-i18n/tests/graceful_fallback_spec.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
|
||||
use dioxus_i18n::prelude::{use_init_i18n, I18n, I18nConfig};
|
||||
use unic_langid::{langid, LanguageIdentifier};
|
||||
|
||||
#[test]
|
||||
fn exact_locale_match_will_use_translation() {
|
||||
test_hook(i18n, |value, proxy| {
|
||||
proxy.assert(
|
||||
value
|
||||
.try_translate("variants")
|
||||
.expect("test message id must exist"),
|
||||
"variants only".to_string(),
|
||||
"exact_locale_match_will_use_translation",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_exact_locale_match_will_use_region() {
|
||||
test_hook(i18n, |value, proxy| {
|
||||
proxy.assert(
|
||||
value
|
||||
.try_translate("region")
|
||||
.expect("test message id must exist"),
|
||||
"region only".to_string(),
|
||||
"non_exact_locale_match_will_use_region",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_exact_locale_match_will_use_script() {
|
||||
test_hook(i18n, |value, proxy| {
|
||||
proxy.assert(
|
||||
value
|
||||
.try_translate("script")
|
||||
.expect("test message id must exist"),
|
||||
"script only".to_string(),
|
||||
"non_exact_locale_match_will_use_script",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_exact_locale_match_will_use_language() {
|
||||
test_hook(i18n, |value, proxy| {
|
||||
proxy.assert(
|
||||
value
|
||||
.try_translate("language")
|
||||
.expect("test message id must exist"),
|
||||
"language only".to_string(),
|
||||
"non_exact_locale_match_will_use_language",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_locale_match_will_use_fallback() {
|
||||
test_hook(i18n, |value, proxy| {
|
||||
proxy.assert(
|
||||
value
|
||||
.try_translate("fallback")
|
||||
.expect("test message id must exist"),
|
||||
"fallback only".to_string(),
|
||||
"no_locale_match_will_use_fallback",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn i18n() -> I18n {
|
||||
const FALLBACK_LANG: LanguageIdentifier = langid!("fb-FB");
|
||||
const LANGUAGE_LANG: LanguageIdentifier = langid!("la");
|
||||
const SCRIPT_LANG: LanguageIdentifier = langid!("la-Scpt");
|
||||
const REGION_LANG: LanguageIdentifier = langid!("la-Scpt-LA");
|
||||
let variants_lang: LanguageIdentifier = langid!("la-Scpt-LA-variants");
|
||||
|
||||
let config = I18nConfig::new(variants_lang.clone())
|
||||
.with_locale((LANGUAGE_LANG, include_str!("../tests/data/fallback/la.ftl")))
|
||||
.with_locale((
|
||||
SCRIPT_LANG,
|
||||
include_str!("../tests/data/fallback/la-Scpt.ftl"),
|
||||
))
|
||||
.with_locale((
|
||||
REGION_LANG,
|
||||
include_str!("../tests/data/fallback/la-Scpt-LA.ftl"),
|
||||
))
|
||||
.with_locale((
|
||||
variants_lang.clone(),
|
||||
include_str!("../tests/data/fallback/la-Scpt-LA-variants.ftl"),
|
||||
))
|
||||
.with_locale((
|
||||
FALLBACK_LANG,
|
||||
include_str!("../tests/data/fallback/fb-FB.ftl"),
|
||||
))
|
||||
.with_fallback(FALLBACK_LANG);
|
||||
use_init_i18n(|| config)
|
||||
}
|
||||
85
dioxus-i18n/tests/macro_reexport_spec.rs
Normal file
85
dioxus-i18n/tests/macro_reexport_spec.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
// Test that macros work correctly when re-exported from another module
|
||||
// This verifies that $crate is used correctly instead of hard-coded dioxus_i18n
|
||||
|
||||
mod reexport_module {
|
||||
// Re-export the macros as if they were from a different crate
|
||||
pub use dioxus_i18n::{t, te, tid};
|
||||
}
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
|
||||
use dioxus_i18n::prelude::{use_init_i18n, I18n, I18nConfig};
|
||||
use unic_langid::{langid, LanguageIdentifier};
|
||||
|
||||
#[test]
|
||||
fn reexported_t_macro_works() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| {
|
||||
let name = "World";
|
||||
reexport_module::t!("hello", name: name)
|
||||
});
|
||||
proxy.assert(panic.is_ok(), true, "reexported_t_macro_works");
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"Hello, \u{2068}World\u{2069}!".to_string(),
|
||||
"reexported_t_macro_works",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reexported_te_macro_works() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| {
|
||||
let name = "World";
|
||||
reexport_module::te!("hello", name: name)
|
||||
});
|
||||
proxy.assert(panic.is_ok(), true, "reexported_te_macro_works");
|
||||
proxy.assert(
|
||||
panic.ok().unwrap().ok().unwrap(),
|
||||
"Hello, \u{2068}World\u{2069}!".to_string(),
|
||||
"reexported_te_macro_works",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reexported_tid_macro_works() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| {
|
||||
let name = "World";
|
||||
reexport_module::tid!("hello", name: name)
|
||||
});
|
||||
proxy.assert(panic.is_ok(), true, "reexported_tid_macro_works");
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"Hello, \u{2068}World\u{2069}!".to_string(),
|
||||
"reexported_tid_macro_works",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reexported_macro_with_invalid_key_as_error() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| reexport_module::te!("invalid"));
|
||||
proxy.assert(
|
||||
panic.is_ok(),
|
||||
true,
|
||||
"reexported_macro_with_invalid_key_as_error",
|
||||
);
|
||||
proxy.assert(
|
||||
panic.ok().unwrap().err().unwrap().to_string(),
|
||||
"message id not found for key: 'invalid'".to_string(),
|
||||
"reexported_macro_with_invalid_key_as_error",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const EN: LanguageIdentifier = langid!("en");
|
||||
|
||||
fn i18n_from_static() -> I18n {
|
||||
let config = I18nConfig::new(EN).with_locale((EN, include_str!("./data/i18n/en.ftl")));
|
||||
use_init_i18n(|| config)
|
||||
}
|
||||
343
dioxus-i18n/tests/translations_spec.rs
Normal file
343
dioxus-i18n/tests/translations_spec.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
|
||||
use dioxus_i18n::{
|
||||
prelude::{use_init_i18n, I18n, I18nConfig},
|
||||
t, te, tid,
|
||||
};
|
||||
use unic_langid::{langid, LanguageIdentifier};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn translate_from_static_source() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| {
|
||||
let name = "World";
|
||||
t!("hello", name: name)
|
||||
});
|
||||
proxy.assert(panic.is_ok(), true, "translate_from_static_source");
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"Hello, \u{2068}World\u{2069}!".to_string(),
|
||||
"translate_from_static_source",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_to_translate_with_invalid_key() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| {
|
||||
let _ = &t!("invalid");
|
||||
});
|
||||
proxy.assert(panic.is_err(), true, "failed_to_translate_with_invalid_key");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_to_translate_with_invalid_key_as_error() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| te!("invalid"));
|
||||
proxy.assert(
|
||||
panic.is_ok(),
|
||||
true,
|
||||
"failed_to_translate_with_invalid_key_as_error",
|
||||
);
|
||||
proxy.assert(
|
||||
panic.ok().unwrap().err().unwrap().to_string(),
|
||||
"message id not found for key: 'invalid'".to_string(),
|
||||
"failed_to_translate_with_invalid_key_as_error",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_to_translate_with_invalid_key_with_args_as_error() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| te!("invalid", name: "<don't care>"));
|
||||
proxy.assert(
|
||||
panic.is_ok(),
|
||||
true,
|
||||
"failed_to_translate_with_invalid_key_with_args_as_error",
|
||||
);
|
||||
proxy.assert(
|
||||
panic.ok().unwrap().err().unwrap().to_string(),
|
||||
"message id not found for key: 'invalid'".to_string(),
|
||||
"failed_to_translate_with_invalid_key_with_args_as_error",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_to_translate_with_invalid_key_as_id() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| tid!("invalid"));
|
||||
proxy.assert(
|
||||
panic.is_ok(),
|
||||
true,
|
||||
"failed_to_translate_with_invalid_key_as_id",
|
||||
);
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"message id not found for key: 'invalid'".to_string(),
|
||||
"failed_to_translate_with_invalid_key_as_id",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_to_translate_with_invalid_key_with_args_as_id() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| tid!("invalid", name: "<don't care>"));
|
||||
proxy.assert(
|
||||
panic.is_ok(),
|
||||
true,
|
||||
"failed_to_translate_with_invalid_key_with_args_as_id",
|
||||
);
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"message id not found for key: 'invalid'".to_string(),
|
||||
"failed_to_translate_with_invalid_key_with_args_as_id",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_root_message_in_attributed_definition() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| tid!("my_component"));
|
||||
proxy.assert(
|
||||
panic.is_ok(),
|
||||
true,
|
||||
"translate_root_message_in_attributed_definition",
|
||||
);
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"My Component".to_string(),
|
||||
"translate_root_message_in_attributed_definition",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_attribute_with_no_args_in_attributed_definition() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| tid!("my_component.placeholder"));
|
||||
proxy.assert(
|
||||
panic.is_ok(),
|
||||
true,
|
||||
"translate_attribute_with_no_args_in_attributed_definition",
|
||||
);
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"Component's placeholder".to_string(),
|
||||
"translate_attribute_with_no_args_in_attributed_definition",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_attribute_with_args_in_attributed_definition() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| tid!("my_component.hint", name: "Zaphod"));
|
||||
proxy.assert(
|
||||
panic.is_ok(),
|
||||
true,
|
||||
"translate_attribute_with_args_in_attributed_definition",
|
||||
);
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"Component's hint with parameter \u{2068}Zaphod\u{2069}".to_string(),
|
||||
"translate_attribute_with_args_in_attributed_definition",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fail_translate_invalid_attribute_with_no_args_in_attributed_definition() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| tid!("my_component.not_a_placeholder"));
|
||||
proxy.assert(
|
||||
panic.is_ok(),
|
||||
true,
|
||||
"fail_translate_invalid_attribute_with_no_args_in_attributed_definition",
|
||||
);
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"attribute id not found for key: 'my_component.not_a_placeholder'".to_string(),
|
||||
"fail_translate_invalid_attribute_with_no_args_in_attributed_definition",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fail_translate_invalid_attribute_with_args_in_attributed_definition() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| tid!("my_component.not_a_hint", name: "Zaphod"));
|
||||
proxy.assert(
|
||||
panic.is_ok(),
|
||||
true,
|
||||
"fail_translate_invalid_attribute_with_args_in_attributed_definition",
|
||||
);
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"attribute id not found for key: 'my_component.not_a_hint'".to_string(),
|
||||
"fail_translate_invalid_attribute_with_args_in_attributed_definition",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fail_translate_with_invalid_attribute_key() {
|
||||
test_hook(i18n_from_static, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| tid!("my_component.placeholder.invalid"));
|
||||
proxy.assert(
|
||||
panic.is_ok(),
|
||||
true,
|
||||
"fail_translate_with_invalid_attribute_key",
|
||||
);
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"invalid message id: 'my_component.placeholder.invalid'".to_string(),
|
||||
"fail_translate_with_invalid_attribute_key",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_from_dynamic_source() {
|
||||
test_hook(i18n_from_dynamic, |_, proxy| {
|
||||
let panic = std::panic::catch_unwind(|| {
|
||||
let name = "World";
|
||||
t!("hello", name: name)
|
||||
});
|
||||
proxy.assert(panic.is_ok(), true, "translate_from_dynamic_source");
|
||||
proxy.assert(
|
||||
panic.ok().unwrap(),
|
||||
"Hello, \u{2068}World\u{2069}!".to_string(),
|
||||
"translate_from_dynamic_source",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
#[ignore] // Panic hidden within test_hook.
|
||||
fn fail_translate_from_dynamic_source_when_file_does_not_exist() {
|
||||
test_hook(i18n_from_dynamic_none_existing, |_, _| unreachable!());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_language_is_set() {
|
||||
test_hook(i18n_from_static, |value, proxy| {
|
||||
proxy.assert(value.language(), EN, "initial_language_is_set");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_can_be_set() {
|
||||
test_hook(i18n_from_static, |mut value, proxy| {
|
||||
value
|
||||
.try_set_language(JP)
|
||||
.expect("set_language must succeed");
|
||||
proxy.assert(value.language(), JP, "language_can_be_set");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_default_fallback_language() {
|
||||
test_hook(i18n_from_static, |value, proxy| {
|
||||
proxy.assert(
|
||||
format!("{:?}", value.fallback_language()),
|
||||
"None".to_string(),
|
||||
"no_default_fallback_language",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn some_default_fallback_language() {
|
||||
test_hook(i18n_from_static_with_fallback, |value, proxy| {
|
||||
proxy.assert(
|
||||
format!("{:?}", value.fallback_language().map(|l| l.to_string())),
|
||||
"Some(\"jp\")".to_string(),
|
||||
"some_default_fallback_language",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_language_can_be_set() {
|
||||
test_hook(i18n_from_static_with_fallback, |mut value, proxy| {
|
||||
value
|
||||
.try_set_fallback_language(EN)
|
||||
.expect("try_set_fallback_language must succeed");
|
||||
proxy.assert(
|
||||
format!("{:?}", value.fallback_language().map(|l| l.to_string())),
|
||||
"Some(\"en\")".to_string(),
|
||||
"fallback_language_can_be_set",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_language_must_have_locale_translation() {
|
||||
test_hook(i18n_from_static_with_fallback, |mut value, proxy| {
|
||||
let result = value.try_set_fallback_language(IT);
|
||||
|
||||
proxy.assert(
|
||||
result.is_err(),
|
||||
true,
|
||||
"fallback_language_must_have_locale_translation",
|
||||
);
|
||||
proxy.assert(
|
||||
result.err().unwrap().to_string(),
|
||||
"fallback for \"it\" must have locale".to_string(),
|
||||
"fallback_language_must_have_locale_translation",
|
||||
);
|
||||
proxy.assert(
|
||||
format!("{:?}", value.fallback_language().map(|l| l.to_string())),
|
||||
"Some(\"jp\")".to_string(),
|
||||
"fallback_language_must_have_locale_translation",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const EN: LanguageIdentifier = langid!("en");
|
||||
const IT: LanguageIdentifier = langid!("it");
|
||||
const JP: LanguageIdentifier = langid!("jp");
|
||||
|
||||
fn i18n_from_static() -> I18n {
|
||||
let config = I18nConfig::new(EN).with_locale((EN, include_str!("./data/i18n/en.ftl")));
|
||||
use_init_i18n(|| config)
|
||||
}
|
||||
|
||||
fn i18n_from_static_with_fallback() -> I18n {
|
||||
let config = I18nConfig::new(EN)
|
||||
.with_locale((EN, include_str!("./data/i18n/en.ftl")))
|
||||
.with_fallback(JP);
|
||||
use_init_i18n(|| config)
|
||||
}
|
||||
|
||||
fn i18n_from_dynamic() -> I18n {
|
||||
let config = I18nConfig::new(EN).with_locale((
|
||||
EN,
|
||||
PathBuf::from(format!(
|
||||
"{}/tests/data/i18n/en.ftl",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
)),
|
||||
));
|
||||
use_init_i18n(|| config)
|
||||
}
|
||||
|
||||
fn i18n_from_dynamic_none_existing() -> I18n {
|
||||
let config = I18nConfig::new(EN).with_locale((
|
||||
EN,
|
||||
PathBuf::from(format!(
|
||||
"{}/tests/data/i18n/non_existing.ftl",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
)),
|
||||
));
|
||||
use_init_i18n(|| config)
|
||||
}
|
||||
Reference in New Issue
Block a user