diff --git a/druid/examples/assets/en-US/banana.ftl b/druid/examples/assets/en-US/banana.ftl new file mode 100644 index 0000000000..985028d024 --- /dev/null +++ b/druid/examples/assets/en-US/banana.ftl @@ -0,0 +1,7 @@ +banana-title = Banana count + +bananas = {$count -> + [0] No bananas + [1] One banana + *[other] {$count} bananas +} diff --git a/druid/examples/assets/fr-FR/banana.ftl b/druid/examples/assets/fr-FR/banana.ftl new file mode 100644 index 0000000000..ce0b516d33 --- /dev/null +++ b/druid/examples/assets/fr-FR/banana.ftl @@ -0,0 +1,7 @@ +banana-title = Nombre de bananes + +bananas = {$count -> + [0] Aucune banane + [1] Une banane + *[other] {$count} bananes +} diff --git a/druid/examples/l10n.rs b/druid/examples/l10n.rs new file mode 100644 index 0000000000..a530de9d5d --- /dev/null +++ b/druid/examples/l10n.rs @@ -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 { + // 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::::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) +} diff --git a/druid/examples/readme.md b/druid/examples/readme.md index b986532de3..0fa2ba4f97 100644 --- a/druid/examples/readme.md +++ b/druid/examples/readme.md @@ -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 diff --git a/druid/src/localization.rs b/druid/src/localization.rs index 188b393161..dddfcbe1ea 100644 --- a/druid/src/localization.rs +++ b/druid/src/localization.rs @@ -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}; @@ -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, - ) -> 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)) } } @@ -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(); @@ -249,7 +256,7 @@ impl L10nManager { &'args self, key: &str, args: impl Into>>, - ) -> Option { + ) -> Option> { let args = args.into(); let value = match self .current_bundle @@ -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::() - .into(), - ) - } else { - Some(result.into()) - } + Some(result) } //TODO: handle locale change } @@ -361,9 +353,16 @@ impl LocalizedString { 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 }