PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.10.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.10.0
4.10.0 4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Framework / Language / Language.php
wp-staging / Framework / Language Last commit date
Language.php 5 days ago
Language.php
429 lines
1 <?php
2
3 namespace WPStaging\Framework\Language;
4
5 use WPStaging\Framework\Facades\Hooks;
6 use WPStaging\Framework\Utils\Env;
7
8 class Language
9 {
10 /** @var string */
11 const HOOK_LOAD_MO_FILES = 'wpstg.language.load_mo_files';
12
13 /** @var string */
14 const TEXT_DOMAIN = 'wp-staging';
15
16 const FILTER_PLUGIN_LOCALE = 'plugin_locale';
17
18 /** @var string */
19 const CLIENT_CLI = 'cli';
20
21 /** @var string */
22 const CLIENT_DESKTOP = 'desktop';
23
24 /** @var string */
25 const DEFAULT_CAMPAIGN = 'pro_upgrade';
26
27 /**
28 * Campaign rather than utm_source or utm_term: campaign name is a top-level
29 * Matomo dimension that archiving never collapses.
30 *
31 * @var array
32 */
33 const CLIENT_CAMPAIGNS = [
34 self::CLIENT_CLI => 'wp-staging-cli',
35 self::CLIENT_DESKTOP => 'wp-staging-desktop',
36 ];
37
38 /**
39 * @return void
40 */
41 public function load()
42 {
43 /** @noinspection NullPointerExceptionInspection */
44 $pluginLangDirectory = WPSTG_PLUGIN_DIR . 'languages/';
45 $wpLangDirectory = $this->getLangDirectory();
46
47 if (function_exists('get_user_locale')) {
48 $locale = get_user_locale();
49 } else {
50 $locale = get_locale();
51 }
52
53 // Traditional WP plugin locale filter
54 $locale = apply_filters(self::FILTER_PLUGIN_LOCALE, $locale, self::TEXT_DOMAIN);
55 $localMoFile = $this->getLocalMoFile($locale);
56 $globalMoFile = $this->getGlobalMoFile($locale);
57 // Unfiltered mo file name
58 $actualMoFile = sprintf('%1$s-%2$s.mo', self::TEXT_DOMAIN, $locale);
59
60 // Setup paths to current locale file
61 $moFileLocal = $pluginLangDirectory . $localMoFile;
62 $moFilesGlobal = [];
63 if ($globalMoFile !== $actualMoFile) {
64 $moFilesGlobal[] = sprintf('%s/%s/%s', $wpLangDirectory, 'plugins', $actualMoFile);
65 }
66
67 $moFilesGlobal[] = sprintf('%s/%s/%s', $wpLangDirectory, 'plugins', $globalMoFile);
68
69 // Internal use only: loads the .mo files
70 Hooks::callInternalHook(self::HOOK_LOAD_MO_FILES, [$locale, $moFileLocal, $moFilesGlobal]);
71 }
72
73 /**
74 * Get the language code of the current locale, e.g. de, en, it, etc.
75 * @return string
76 */
77 public function getLocaleLanguageCode(): string
78 {
79 if (function_exists('get_user_locale')) {
80 $locale = get_user_locale();
81 } else {
82 $locale = get_locale();
83 }
84 return substr($locale, 0, 2);
85 }
86
87 /**
88 * Locale prefix/code to the short code used in our .mo file names.
89 * Order matters: a longer prefix must precede any shorter one it overlaps,
90 * so a future 'zh_' entry would have to sit after 'zh_CN'.
91 */
92 const LOCALE_TO_FILE_CODE = [
93 'de_' => 'de',
94 'es_' => 'es',
95 'fr_' => 'fr',
96 'it_' => 'it',
97 'nl_' => 'nl',
98 'pl_' => 'pl',
99 'ru_' => 'ru',
100 'tr_' => 'tr',
101 'pt_BR' => 'pt_BR',
102 'zh_CN' => 'zh_CN',
103 'ja' => 'ja',
104 ];
105
106 /** Short file code to the full WordPress locale used by global .mo files. */
107 const FILE_CODE_TO_GLOBAL_LOCALE = [
108 'de' => 'de_DE',
109 'es' => 'es_ES',
110 'fr' => 'fr_FR',
111 'it' => 'it_IT',
112 'nl' => 'nl_NL',
113 'pl' => 'pl_PL',
114 'ru' => 'ru_RU',
115 'tr' => 'tr_TR',
116 'pt_BR' => 'pt_BR',
117 'zh_CN' => 'zh_CN',
118 'ja' => 'ja',
119 ];
120
121 /**
122 * Resolve a WordPress locale to the language code used in our bundled .mo files.
123 *
124 * @param string $locale
125 * @return string|null Null when no bundled translation exists.
126 */
127 private function resolveFileCode(string $locale)
128 {
129 foreach (self::LOCALE_TO_FILE_CODE as $prefix => $code) {
130 if (strpos($locale, $prefix) === 0 || $locale === $code) {
131 return $code;
132 }
133 }
134
135 return null;
136 }
137
138 protected function getLocalMoFile(string $locale): string
139 {
140 $code = $this->resolveFileCode($locale);
141 if ($code !== null) {
142 $locale = $code;
143 }
144
145 return sprintf('%1$s-%2$s.mo', self::TEXT_DOMAIN, $locale);
146 }
147
148 protected function getGlobalMoFile(string $locale): string
149 {
150 $code = $this->resolveFileCode($locale);
151 if ($code !== null && isset(self::FILE_CODE_TO_GLOBAL_LOCALE[$code])) {
152 $locale = self::FILE_CODE_TO_GLOBAL_LOCALE[$code];
153 }
154
155 return sprintf('%1$s-%2$s.mo', self::TEXT_DOMAIN, $locale);
156 }
157
158 /**
159 * Rewrite a checkout URL for the current locale.
160 * German locales (de_DE, de_AT, de_CH, de_DE_formal, …) use /de/kaufen/ instead of /checkout/.
161 */
162 public static function localizeCheckoutUrl(string $url): string
163 {
164 $locale = function_exists('get_user_locale') ? get_user_locale() : get_locale();
165 if (strpos($locale, 'de_') === 0) {
166 return str_replace('/checkout/', '/de/kaufen/', $url);
167 }
168
169 return $url;
170 }
171
172 /**
173 * Rewrite a pricing URL for the current locale.
174 * German locales use /de/#pricing instead of /#pricing.
175 */
176 public static function localizePricingUrl(string $url): string
177 {
178 $locale = function_exists('get_user_locale') ? get_user_locale() : get_locale();
179 if (strpos($locale, 'de_') === 0) {
180 return str_replace('wp-staging.com/#', 'wp-staging.com/de/#', $url);
181 }
182
183 return $url;
184 }
185
186 /**
187 * Build the localized wp-staging.com pricing-table URL for an in-plugin
188 * "upgrade to Pro" CTA.
189 *
190 * The language path follows the admin user's locale, falling back to the site
191 * locale, so users land on the pricing table in their own language.
192 *
193 * @param string $context Optional utm_content slug identifying the link.
194 * Sanitized to [a-z0-9_].
195 * @param string $source utm_source for the click; pass a Pro/licensing source
196 * for CTAs shown to licensed users. Sanitized to
197 * [a-z0-9_-]; empty input falls back to the default.
198 */
199 public static function getUpgradeUrl(string $context = '', string $source = 'wp-staging-free'): string
200 {
201 // wp-staging.com ships only these languages; every other locale falls back
202 // to the English pricing table at "/".
203 $localePaths = [
204 'de' => '/de/',
205 'it' => '/it/',
206 'es' => '/es/',
207 'fr' => '/fr/',
208 'pt' => '/pt/',
209 'pl' => '/pl/',
210 'ja' => '/ja/',
211 ];
212
213 $locale = function_exists('get_user_locale') ? get_user_locale() : get_locale();
214 $prefix = strtolower(substr($locale, 0, 2));
215 $path = isset($localePaths[$prefix]) ? $localePaths[$prefix] : '/';
216
217 $base = 'https://wp-staging.com' . $path;
218
219 $context = preg_replace('/[^a-z0-9_]/', '', strtolower($context));
220 if ($context === '') {
221 return $base . '#pricing';
222 }
223
224 $source = preg_replace('/[^a-z0-9_-]/', '', strtolower($source));
225 if ($source === '') {
226 $source = 'wp-staging-free';
227 }
228
229 // Query must precede the #pricing anchor, or the link stops both tracking
230 // and scrolling to the pricing table.
231 return $base . '?' . self::buildUtmQuery($context, $source) . '#pricing';
232 }
233
234 /**
235 * @param string $context Already sanitized utm_content slug.
236 * @param string $source Already sanitized utm_source slug.
237 * @return string
238 */
239 private static function buildUtmQuery(string $context, string $source): string
240 {
241 return http_build_query([
242 'utm_source' => $source,
243 'utm_medium' => 'plugin',
244 'utm_campaign' => self::getUpgradeCampaign(),
245 'utm_content' => $context,
246 ]);
247 }
248
249 /**
250 * @param string $context Optional utm_content slug, sanitized to [a-z0-9_].
251 */
252 public static function getDesktopUrl(string $context = ''): string
253 {
254 $url = self::localizeUrl('https://wp-staging.com/desktop/');
255 $context = preg_replace('/[^a-z0-9_]/', '', strtolower($context));
256
257 if ($context === '') {
258 return $url;
259 }
260
261 return $url . '?' . self::buildUtmQuery($context, 'wp-staging-free');
262 }
263
264 /**
265 * Which WP STAGING environment serves this install: CLIENT_CLI, CLIENT_DESKTOP,
266 * or '' for an ordinary host. The CLI writes WPSTG_CLIENT into the php service
267 * of the site's docker-compose.yml.
268 */
269 public static function getInstallClient(): string
270 {
271 $client = Env::get('WPSTG_CLIENT');
272 if (!is_string($client)) {
273 return '';
274 }
275
276 $client = strtolower(trim($client));
277
278 return array_key_exists($client, self::CLIENT_CAMPAIGNS) ? $client : '';
279 }
280
281 /**
282 * utm_campaign for this install: the environment's own campaign when the
283 * site runs on the CLI or Desktop stack, the generic one everywhere else.
284 */
285 public static function getUpgradeCampaign(): string
286 {
287 $client = self::getInstallClient();
288
289 return $client === '' ? self::DEFAULT_CAMPAIGN : self::CLIENT_CAMPAIGNS[$client];
290 }
291
292 /**
293 * Re-tag an already-campaigned wp-staging.com URL with this install's environment,
294 * for CTAs that build their URL by hand instead of going through getUpgradeUrl().
295 * A URL without a utm_campaign is left untouched, so a plain docs link never
296 * becomes a campaign one.
297 */
298 public static function addClientAttribution(string $url): string
299 {
300 $client = self::getInstallClient();
301 if ($client === '' || strpos($url, 'wp-staging.com') === false) {
302 return $url;
303 }
304
305 // Fragment must trail the query string, or the link stops both tracking
306 // and scrolling.
307 $fragment = '';
308 $hashPos = strpos($url, '#');
309 if ($hashPos !== false) {
310 $fragment = substr($url, $hashPos);
311 $url = substr($url, 0, $hashPos);
312 }
313
314 $queryPos = strpos($url, '?');
315 if ($queryPos === false) {
316 return $url . $fragment;
317 }
318
319 $args = [];
320 parse_str(substr($url, $queryPos + 1), $args);
321 if (empty($args['utm_campaign'])) {
322 return $url . $fragment;
323 }
324
325 if (empty($args['utm_content'])) {
326 $args['utm_content'] = $args['utm_campaign'];
327 }
328
329 $args['utm_campaign'] = self::CLIENT_CAMPAIGNS[$client];
330
331 return substr($url, 0, $queryPos) . '?' . http_build_query($args) . $fragment;
332 }
333
334 /**
335 * Rewrite the support URL for the current locale.
336 * German locales use /de/support/ instead of /support/.
337 */
338 public static function localizeSupportUrl(string $url): string
339 {
340 $locale = function_exists('get_user_locale') ? get_user_locale() : get_locale();
341 if (strpos($locale, 'de_') === 0) {
342 return str_replace('/support/', '/de/support/', $url);
343 }
344
345 return $url;
346 }
347
348 /**
349 * Rewrite a wp-staging.com homepage URL for the current locale.
350 * German locales insert /de/ after the domain.
351 */
352 public static function localizeHomepageUrl(string $url): string
353 {
354 $locale = function_exists('get_user_locale') ? get_user_locale() : get_locale();
355 if (strpos($locale, 'de_') === 0) {
356 return str_replace('wp-staging.com/', 'wp-staging.com/de/', $url);
357 }
358
359 return $url;
360 }
361
362 /**
363 * Rewrite any wp-staging.com URL for the current locale.
364 * Inserts /de/ after the domain for German locales.
365 * Works with bare URLs, URLs with paths, and fragment-only URLs like /#pricing.
366 */
367 public static function localizeUrl(string $url): string
368 {
369 $locale = function_exists('get_user_locale') ? get_user_locale() : get_locale();
370 if (strpos($locale, 'de_') !== 0) {
371 return $url;
372 }
373
374 if (strpos($url, 'wp-staging.com/de/') !== false) {
375 return $url;
376 }
377
378 return preg_replace(
379 '#(https?://wp-staging\.com)/?#',
380 '$1/de/',
381 $url,
382 1
383 );
384 }
385
386 /**
387 * Rewrite a wp-staging.com docs URL for the current locale.
388 * Handles articles where the German slug differs from the English one.
389 */
390 public static function localizeDocsUrl(string $url): string
391 {
392 $locale = function_exists('get_user_locale') ? get_user_locale() : get_locale();
393 if (strpos($locale, 'de_') !== 0) {
394 return $url;
395 }
396
397 $germanDocsMap = [
398 'https://wp-staging.com/docs/how-to-migrate-your-wordpress-site-to-a-new-host/' => 'https://wp-staging.com/de/docs/wordpress-seite-zu-anderem-host-migrieren/',
399 'https://wp-staging.com/docs/documentation/' => 'https://wp-staging.com/de/docs/dokumentation/',
400 'https://wp-staging.com/docs/set-up-wp-staging-cli/' => 'https://wp-staging.com/de/docs/lokale-kopie-deiner-wordpress-seite-erstellen/',
401 'https://wp-staging.com/docs/pull-a-wordpress-site-from-one-server-to-another/' => 'https://wp-staging.com/de/docs/wordpress-seite-von-einem-server-auf-einen-anderen-ziehen/',
402 ];
403
404 // Strip fragment for lookup, re-append after
405 $fragment = '';
406 $hashPos = strpos($url, '#');
407 if ($hashPos !== false) {
408 $fragment = substr($url, $hashPos);
409 $baseUrl = substr($url, 0, $hashPos);
410 } else {
411 $baseUrl = $url;
412 }
413
414 if (isset($germanDocsMap[$baseUrl])) {
415 return $germanDocsMap[$baseUrl] . $fragment;
416 }
417
418 return self::localizeUrl($url);
419 }
420
421 /**
422 * @return string
423 */
424 protected function getLangDirectory(): string
425 {
426 return WP_LANG_DIR;
427 }
428 }
429