ferrocat_po/api/
pseudolocalization.rs1use ferrocat_icu::{IcuPseudolocalizationOptions, pseudolocalize_icu_message, stringify_icu};
2
3use super::icu_syntax::parse_icu_with_syntax_policy;
4use super::{ApiError, CompiledCatalogArtifact, IcuSyntaxPolicy};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[non_exhaustive]
9pub struct CompiledCatalogPseudolocalizationOptions<'a> {
10 pub icu_options: IcuPseudolocalizationOptions<'a>,
12 pub syntax_policy: IcuSyntaxPolicy,
14}
15
16impl Default for CompiledCatalogPseudolocalizationOptions<'_> {
17 fn default() -> Self {
18 Self {
19 icu_options: IcuPseudolocalizationOptions::new(),
20 syntax_policy: IcuSyntaxPolicy::Strict,
21 }
22 }
23}
24
25impl<'a> CompiledCatalogPseudolocalizationOptions<'a> {
26 #[must_use]
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 #[must_use]
34 pub fn with_icu_options(mut self, icu_options: IcuPseudolocalizationOptions<'a>) -> Self {
35 self.icu_options = icu_options;
36 self
37 }
38
39 #[must_use]
41 pub fn with_syntax_policy(mut self, syntax_policy: IcuSyntaxPolicy) -> Self {
42 self.syntax_policy = syntax_policy;
43 self
44 }
45}
46
47pub fn pseudolocalize_compiled_catalog_artifact(
59 artifact: &CompiledCatalogArtifact,
60 options: &CompiledCatalogPseudolocalizationOptions<'_>,
61) -> Result<CompiledCatalogArtifact, ApiError> {
62 let mut pseudolocalized = artifact.clone();
63 for (key, message) in &mut pseudolocalized.messages {
64 let parsed =
65 parse_icu_with_syntax_policy(message, options.syntax_policy).map_err(|error| {
66 ApiError::Unsupported(format!(
67 "compiled catalog artifact message {key:?} cannot be pseudolocalized: {error}"
68 ))
69 })?;
70 *message = stringify_icu(&pseudolocalize_icu_message(&parsed, &options.icu_options));
71 }
72 Ok(pseudolocalized)
73}
74
75#[cfg(test)]
76mod tests {
77 use std::collections::BTreeMap;
78
79 use ferrocat_icu::IcuPseudolocalizationOptions;
80
81 use super::{
82 CompiledCatalogPseudolocalizationOptions, pseudolocalize_compiled_catalog_artifact,
83 };
84 use crate::api::{
85 ApiError, CatalogMessageKey, CompiledCatalogArtifact, CompiledCatalogMissingMessage,
86 IcuSyntaxPolicy,
87 };
88
89 #[test]
90 fn pseudolocalize_compiled_artifact_transforms_messages_only() {
91 let artifact = CompiledCatalogArtifact {
92 messages: BTreeMap::from([("runtime-key".to_owned(), "Hello {name}".to_owned())]),
93 missing: vec![CompiledCatalogMissingMessage {
94 key: "runtime-key".to_owned(),
95 source_key: CatalogMessageKey::new("Hello {name}", None),
96 requested_locale: "qps".to_owned(),
97 resolved_locale: Some("en".to_owned()),
98 }],
99 diagnostics: Vec::new(),
100 };
101
102 let pseudolocalized = pseudolocalize_compiled_catalog_artifact(
103 &artifact,
104 &CompiledCatalogPseudolocalizationOptions::new()
105 .with_icu_options(IcuPseudolocalizationOptions::new().with_expansion_percent(0)),
106 )
107 .expect("pseudolocalize artifact");
108
109 assert_eq!(
110 pseudolocalized.messages["runtime-key"],
111 "[!! Ĥéļļö {name} !!]"
112 );
113 assert_eq!(pseudolocalized.missing, artifact.missing);
114 assert_eq!(pseudolocalized.diagnostics, artifact.diagnostics);
115 }
116
117 #[test]
118 fn pseudolocalize_compiled_artifact_reports_invalid_runtime_messages() {
119 let artifact = CompiledCatalogArtifact {
120 messages: BTreeMap::from([("runtime-key".to_owned(), "{broken".to_owned())]),
121 missing: Vec::new(),
122 diagnostics: Vec::new(),
123 };
124
125 let error = pseudolocalize_compiled_catalog_artifact(
126 &artifact,
127 &CompiledCatalogPseudolocalizationOptions::new(),
128 )
129 .expect_err("invalid runtime message");
130
131 assert!(
132 matches!(error, ApiError::Unsupported(message) if message.contains("message \"runtime-key\" cannot be pseudolocalized"))
133 );
134 }
135
136 #[test]
137 fn pseudolocalize_compiled_artifact_uses_runtime_apostrophe_policy() {
138 let artifact = CompiledCatalogArtifact {
139 messages: BTreeMap::from([("runtime-key".to_owned(), "You're {name}".to_owned())]),
140 missing: Vec::new(),
141 diagnostics: Vec::new(),
142 };
143
144 let pseudolocalized = pseudolocalize_compiled_catalog_artifact(
145 &artifact,
146 &CompiledCatalogPseudolocalizationOptions::new()
147 .with_icu_options(IcuPseudolocalizationOptions::new().with_expansion_percent(0))
148 .with_syntax_policy(IcuSyntaxPolicy::RuntimeLiteralApostrophes),
149 )
150 .expect("pseudolocalize artifact");
151
152 assert!(pseudolocalized.messages["runtime-key"].contains("{name}"));
153 assert_ne!(
154 pseudolocalized.messages["runtime-key"],
155 artifact.messages["runtime-key"]
156 );
157 }
158
159 #[test]
160 fn pseudolocalize_compiled_artifact_preserves_runtime_quotes() {
161 let artifact = CompiledCatalogArtifact {
162 messages: BTreeMap::from([
163 ("quoted-argument".to_owned(), "L'{title}".to_owned()),
164 (
165 "quoted-pound".to_owned(),
166 "{count, plural, other {'#' items}}".to_owned(),
167 ),
168 ]),
169 missing: Vec::new(),
170 diagnostics: Vec::new(),
171 };
172
173 let pseudolocalized = pseudolocalize_compiled_catalog_artifact(
174 &artifact,
175 &CompiledCatalogPseudolocalizationOptions::new()
176 .with_icu_options(IcuPseudolocalizationOptions::new().with_expansion_percent(0))
177 .with_syntax_policy(IcuSyntaxPolicy::RuntimeLiteralApostrophes),
178 )
179 .expect("pseudolocalize artifact");
180 let quoted_argument = &pseudolocalized.messages["quoted-argument"];
181 let parsed =
182 ferrocat_icu::parse_icu(quoted_argument).expect("canonical pseudolocalized ICU");
183
184 assert!(!ferrocat_icu::extract_argument_names(&parsed).contains(&"title".to_owned()));
185 assert!(pseudolocalized.messages["quoted-pound"].contains("'#'"));
186 }
187}