|
| 1 | +use std::collections::HashSet; |
1 | 2 | use std::path::Path; |
2 | 3 |
|
3 | 4 | use serde::{Deserialize, Serialize}; |
| 5 | +use serde_json::Value; |
4 | 6 |
|
5 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] |
6 | 8 | #[serde(rename_all = "camelCase")] |
@@ -52,15 +54,44 @@ impl I18nConfig { |
52 | 54 |
|
53 | 55 | for config_path in config_paths { |
54 | 56 | if let Ok(content) = std::fs::read_to_string(&config_path) { |
55 | | - if let Ok(config) = serde_json::from_str::<I18nConfig>(&content) { |
| 57 | + let raw_config = serde_json::from_str::<Value>(&content).ok(); |
| 58 | + |
| 59 | + if let Ok(mut config) = serde_json::from_str::<I18nConfig>(&content) { |
| 60 | + let has_locale_paths = raw_config |
| 61 | + .as_ref() |
| 62 | + .and_then(|value| value.as_object()) |
| 63 | + .is_some_and(|object| { |
| 64 | + object.contains_key("localePaths") || object.contains_key("locale_paths") |
| 65 | + }); |
| 66 | + |
| 67 | + if !has_locale_paths { |
| 68 | + config.add_detected_locale_paths(root); |
| 69 | + } |
| 70 | + |
56 | 71 | tracing::info!("Loaded config from {:?}", config_path); |
57 | 72 | return config; |
58 | 73 | } |
59 | 74 | } |
60 | 75 | } |
61 | 76 |
|
62 | 77 | tracing::info!("Using default config"); |
63 | | - Self::default() |
| 78 | + let mut config = Self::default(); |
| 79 | + config.add_detected_locale_paths(root); |
| 80 | + config |
| 81 | + } |
| 82 | + |
| 83 | + fn add_detected_locale_paths(&mut self, root: &Path) { |
| 84 | + let detected_paths = detect_framework_locale_paths(root); |
| 85 | + if detected_paths.is_empty() { |
| 86 | + return; |
| 87 | + } |
| 88 | + |
| 89 | + let mut existing: HashSet<String> = self.locale_paths.iter().cloned().collect(); |
| 90 | + for path in detected_paths { |
| 91 | + if existing.insert(path.clone()) { |
| 92 | + self.locale_paths.push(path); |
| 93 | + } |
| 94 | + } |
64 | 95 | } |
65 | 96 | } |
66 | 97 |
|
@@ -93,5 +124,66 @@ fn default_function_patterns() -> Vec<String> { |
93 | 124 | r#"translate(?:Service)?\.(?:instant|get|stream)\s*\(\s*["']([^"']+)["']"#.to_string(), |
94 | 125 | r#"transloco(?:Service)?\.(?:translate|selectTranslate)\s*\(\s*["']([^"']+)["']"#.to_string(), |
95 | 126 | r#"["']([^"']+)["']\s*\|\s*(?:translate|transloco)\b"#.to_string(), |
| 127 | + r#"__\s*\(\s*["']([^"']+)["']"#.to_string(), |
| 128 | + r#"trans(?:_choice)?\s*\(\s*["']([^"']+)["']"#.to_string(), |
| 129 | + r#"Lang::(?:get|choice)\s*\(\s*["']([^"']+)["']"#.to_string(), |
| 130 | + r#"@lang\s*\(\s*["']([^"']+)["']"#.to_string(), |
| 131 | + r#"@choice\s*\(\s*["']([^"']+)["']"#.to_string(), |
96 | 132 | ] |
97 | 133 | } |
| 134 | + |
| 135 | +fn detect_framework_locale_paths(root: &Path) -> Vec<String> { |
| 136 | + let mut paths = Vec::new(); |
| 137 | + |
| 138 | + if is_angular_project(root) { |
| 139 | + paths.push("src/assets/i18n".to_string()); |
| 140 | + } |
| 141 | + |
| 142 | + if is_laravel_project(root) { |
| 143 | + paths.push("resources/lang".to_string()); |
| 144 | + paths.push("lang".to_string()); |
| 145 | + } |
| 146 | + |
| 147 | + paths |
| 148 | + .into_iter() |
| 149 | + .filter(|path| root.join(path).exists()) |
| 150 | + .collect() |
| 151 | +} |
| 152 | + |
| 153 | +fn is_angular_project(root: &Path) -> bool { |
| 154 | + let package_json = root.join("package.json"); |
| 155 | + let Some(value) = read_json(&package_json) else { |
| 156 | + return false; |
| 157 | + }; |
| 158 | + |
| 159 | + json_has_dependency(&value, "@angular/core", &["dependencies", "devDependencies"]) |
| 160 | + || json_has_dependency(&value, "@angular/cli", &["dependencies", "devDependencies"]) |
| 161 | +} |
| 162 | + |
| 163 | +fn is_laravel_project(root: &Path) -> bool { |
| 164 | + let composer_json = root.join("composer.json"); |
| 165 | + let Some(value) = read_json(&composer_json) else { |
| 166 | + return false; |
| 167 | + }; |
| 168 | + |
| 169 | + json_has_dependency(&value, "laravel/framework", &["require", "require-dev"]) |
| 170 | + || json_has_name(&value, "laravel/laravel") |
| 171 | +} |
| 172 | + |
| 173 | +fn read_json(path: &Path) -> Option<Value> { |
| 174 | + let content = std::fs::read_to_string(path).ok()?; |
| 175 | + serde_json::from_str::<Value>(&content).ok() |
| 176 | +} |
| 177 | + |
| 178 | +fn json_has_dependency(value: &Value, dependency: &str, sections: &[&str]) -> bool { |
| 179 | + sections.iter().any(|section| { |
| 180 | + value |
| 181 | + .get(*section) |
| 182 | + .and_then(|deps| deps.as_object()) |
| 183 | + .is_some_and(|deps| deps.contains_key(dependency)) |
| 184 | + }) |
| 185 | +} |
| 186 | + |
| 187 | +fn json_has_name(value: &Value, name: &str) -> bool { |
| 188 | + value.get("name").and_then(|v| v.as_str()) == Some(name) |
| 189 | +} |
0 commit comments