Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

L10n example and performance improvements #2313

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions druid/examples/assets/en-US/banana.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
banana-title = Banana count

bananas = {$count ->
[0] No bananas
[1] One banana
*[other] {$count} bananas
}
7 changes: 7 additions & 0 deletions druid/examples/assets/fr-FR/banana.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
banana-title = Nombre de bananes

bananas = {$count ->
[0] Aucune banane
[1] Une banane
*[other] {$count} bananes
}
76 changes: 76 additions & 0 deletions druid/examples/l10n.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2019 The Druid Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! This is an example of how to translate and localize a druid application.
//! It uses the fluent (.ftl) files in the asset directory for defining defining messages.

// On Windows platform, don't show a console when opening the app.
#![windows_subsystem = "windows"]

use druid::widget::{prelude::*, Slider};
use druid::widget::{Flex, Label};
use druid::{AppLauncher, Data, Lens, LocalizedString, UnitPoint, WidgetExt, WindowDesc};

const VERTICAL_WIDGET_SPACING: f64 = 20.0;
const SLIDER_WIDTH: f64 = 200.0;

#[derive(Clone, Data, Lens)]
struct BananaState {
count: f64,
}

pub fn main() {
let main_window = WindowDesc::new(build_root_widget())
.title(LocalizedString::new("banana-title"))
.window_size((400.0, 400.0));

let initial_state: BananaState = BananaState { count: 1f64 };

// start the application, referencing the translation files in /assets.
AppLauncher::with_window(main_window)
.log_to_console()
.localization_resources(vec!["banana.ftl".into()], "assets".into())
.launch(initial_state)
.expect("Failed to launch application");
}

fn build_root_widget() -> impl Widget<BananaState> {
// create a label with a static translation
let title = Label::new(LocalizedString::new("banana-title")).with_text_size(28.0);

// create a label that uses a translation with dynamic arguments
let banana_label = Label::new(|data: &BananaState, env: &Env| {
let mut s = LocalizedString::<BananaState>::new("bananas")
.with_arg("count", |d, _e| d.count.into());
s.resolve(data, env);

s.localized_str()
})
.with_text_size(32.0);

// control the banana count
let slider = Slider::new()
.with_range(0.0, 3.0)
.with_step(1.0)
.fix_width(SLIDER_WIDTH)
.lens(BananaState::count);

Flex::column()
.with_child(title)
.with_spacer(VERTICAL_WIDGET_SPACING * 2.0)
.with_child(banana_label)
.with_spacer(VERTICAL_WIDGET_SPACING)
.with_child(slider)
.align_vertical(UnitPoint::CENTER)
}
7 changes: 7 additions & 0 deletions druid/examples/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ cargo run --example invalidation --features="im"
```
A demonstration how to use debug invalidation regions in your own widgets, including some examples of builtin widgets.

## L10n
```
cd druid/examples
LANG=fr-FR cargo run --example l10n
```
Shows how to localize and translate text in druid. On Linux, set the LANG environment variable to either "fr-FR" or "en-US".

## Layout
```
cargo run --example layout
Expand Down
53 changes: 26 additions & 27 deletions druid/src/localization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
//! [`Env`]: struct.Env.html
//! [`Data`]: trait.Data.html

use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use std::{fs, io};
Expand Down Expand Up @@ -104,19 +105,19 @@ impl BundleStack {
self.0.iter().flat_map(|b| b.get_message(id)).next()
}

fn format_pattern(
&self,
fn format_pattern<'bundle>(
&'bundle self,
id: &str,
pattern: &FluentPattern<&str>,
args: Option<&FluentArgs>,
pattern: &'bundle FluentPattern<&str>,
args: Option<&'bundle FluentArgs>,
errors: &mut Vec<FluentError>,
) -> String {
) -> Cow<'bundle, str> {
for bundle in self.0.iter() {
if bundle.has_message(id) {
return bundle.format_pattern(pattern, args, errors).to_string();
return bundle.format_pattern(pattern, args, errors);
}
}
format!("localization failed for key '{}'", id)
Cow::Owned(format!("localization failed for key '{}'", id))
}
}

Expand Down Expand Up @@ -155,6 +156,12 @@ impl ResourceManager {
let mut stack = Vec::new();
for locale in &resolved_locales {
let mut bundle = FluentBundle::new(resolved_locales.clone());

// fluent inserts bidi controls when interpolating, and they can
// cause rendering issues; for now we just don't use them.
// https://www.w3.org/International/questions/qa-bidi-unicode-controls#basedirection
bundle.set_use_isolating(false);

for res_id in resource_ids {
let res = self.get_resource(res_id, &locale.to_string());
bundle.add_resource(res).unwrap();
Expand Down Expand Up @@ -249,7 +256,7 @@ impl L10nManager {
&'args self,
key: &str,
args: impl Into<Option<&'args FluentArgs<'args>>>,
) -> Option<ArcStr> {
) -> Option<Cow<str>> {
let args = args.into();
let value = match self
.current_bundle
Expand All @@ -267,22 +274,7 @@ impl L10nManager {
warn!("localization error {:?}", err);
}

// fluent inserts bidi controls when interpolating, and they can
// cause rendering issues; for now we just strip them.
// https://www.w3.org/International/questions/qa-bidi-unicode-controls#basedirection
const START_ISOLATE: char = '\u{2068}';
const END_ISOLATE: char = '\u{2069}';
if args.is_some() && result.chars().any(|c| c == START_ISOLATE) {
Some(
result
.chars()
.filter(|c| c != &START_ISOLATE && c != &END_ISOLATE)
.collect::<String>()
.into(),
)
} else {
Some(result.into())
}
Some(result)
}
//TODO: handle locale change
}
Expand Down Expand Up @@ -361,9 +353,16 @@ impl<T> LocalizedString<T> {

self.resolved_lang = Some(manager.current_locale.clone());
let next = manager.localize(self.key, args.as_ref());
let result = next != self.resolved;
self.resolved = next;
result
{
let next = next.as_ref().map(|cow| cow.as_ref());
let prev = self.resolved.as_ref().map(|arc| arc.as_ref());
if next == prev {
// still the same value, no need to update the field
return false;
}
}
self.resolved = next.map(|cow| cow.into());
true
} else {
false
}
Expand Down