PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.9.5
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.9.5
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
406 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 $query = http_build_query([
232 'utm_source' => $source,
233 'utm_medium' => 'plugin',
234 'utm_campaign' => self::getUpgradeCampaign(),
235 'utm_content' => $context,
236 ]);
237
238 return $base . '?' . $query . '#pricing';
239 }
240
241 /**
242 * Which WP STAGING environment serves this install: CLIENT_CLI, CLIENT_DESKTOP,
243 * or '' for an ordinary host. The CLI writes WPSTG_CLIENT into the php service
244 * of the site's docker-compose.yml.
245 */
246 public static function getInstallClient(): string
247 {
248 $client = Env::get('WPSTG_CLIENT');
249 if (!is_string($client)) {
250 return '';
251 }
252
253 $client = strtolower(trim($client));
254
255 return array_key_exists($client, self::CLIENT_CAMPAIGNS) ? $client : '';
256 }
257
258 /**
259 * utm_campaign for this install: the environment's own campaign when the
260 * site runs on the CLI or Desktop stack, the generic one everywhere else.
261 */
262 public static function getUpgradeCampaign(): string
263 {
264 $client = self::getInstallClient();
265
266 return $client === '' ? self::DEFAULT_CAMPAIGN : self::CLIENT_CAMPAIGNS[$client];
267 }
268
269 /**
270 * Re-tag an already-campaigned wp-staging.com URL with this install's environment,
271 * for CTAs that build their URL by hand instead of going through getUpgradeUrl().
272 * A URL without a utm_campaign is left untouched, so a plain docs link never
273 * becomes a campaign one.
274 */
275 public static function addClientAttribution(string $url): string
276 {
277 $client = self::getInstallClient();
278 if ($client === '' || strpos($url, 'wp-staging.com') === false) {
279 return $url;
280 }
281
282 // Fragment must trail the query string, or the link stops both tracking
283 // and scrolling.
284 $fragment = '';
285 $hashPos = strpos($url, '#');
286 if ($hashPos !== false) {
287 $fragment = substr($url, $hashPos);
288 $url = substr($url, 0, $hashPos);
289 }
290
291 $queryPos = strpos($url, '?');
292 if ($queryPos === false) {
293 return $url . $fragment;
294 }
295
296 $args = [];
297 parse_str(substr($url, $queryPos + 1), $args);
298 if (empty($args['utm_campaign'])) {
299 return $url . $fragment;
300 }
301
302 if (empty($args['utm_content'])) {
303 $args['utm_content'] = $args['utm_campaign'];
304 }
305
306 $args['utm_campaign'] = self::CLIENT_CAMPAIGNS[$client];
307
308 return substr($url, 0, $queryPos) . '?' . http_build_query($args) . $fragment;
309 }
310
311 /**
312 * Rewrite the support URL for the current locale.
313 * German locales use /de/support/ instead of /support/.
314 */
315 public static function localizeSupportUrl(string $url): string
316 {
317 $locale = function_exists('get_user_locale') ? get_user_locale() : get_locale();
318 if (strpos($locale, 'de_') === 0) {
319 return str_replace('/support/', '/de/support/', $url);
320 }
321
322 return $url;
323 }
324
325 /**
326 * Rewrite a wp-staging.com homepage URL for the current locale.
327 * German locales insert /de/ after the domain.
328 */
329 public static function localizeHomepageUrl(string $url): string
330 {
331 $locale = function_exists('get_user_locale') ? get_user_locale() : get_locale();
332 if (strpos($locale, 'de_') === 0) {
333 return str_replace('wp-staging.com/', 'wp-staging.com/de/', $url);
334 }
335
336 return $url;
337 }
338
339 /**
340 * Rewrite any wp-staging.com URL for the current locale.
341 * Inserts /de/ after the domain for German locales.
342 * Works with bare URLs, URLs with paths, and fragment-only URLs like /#pricing.
343 */
344 public static function localizeUrl(string $url): string
345 {
346 $locale = function_exists('get_user_locale') ? get_user_locale() : get_locale();
347 if (strpos($locale, 'de_') !== 0) {
348 return $url;
349 }
350
351 if (strpos($url, 'wp-staging.com/de/') !== false) {
352 return $url;
353 }
354
355 return preg_replace(
356 '#(https?://wp-staging\.com)/?#',
357 '$1/de/',
358 $url,
359 1
360 );
361 }
362
363 /**
364 * Rewrite a wp-staging.com docs URL for the current locale.
365 * Handles articles where the German slug differs from the English one.
366 */
367 public static function localizeDocsUrl(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 $germanDocsMap = [
375 '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/',
376 'https://wp-staging.com/docs/documentation/' => 'https://wp-staging.com/de/docs/dokumentation/',
377 'https://wp-staging.com/docs/set-up-wp-staging-cli/' => 'https://wp-staging.com/de/docs/lokale-kopie-deiner-wordpress-seite-erstellen/',
378 '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/',
379 ];
380
381 // Strip fragment for lookup, re-append after
382 $fragment = '';
383 $hashPos = strpos($url, '#');
384 if ($hashPos !== false) {
385 $fragment = substr($url, $hashPos);
386 $baseUrl = substr($url, 0, $hashPos);
387 } else {
388 $baseUrl = $url;
389 }
390
391 if (isset($germanDocsMap[$baseUrl])) {
392 return $germanDocsMap[$baseUrl] . $fragment;
393 }
394
395 return self::localizeUrl($url);
396 }
397
398 /**
399 * @return string
400 */
401 protected function getLangDirectory(): string
402 {
403 return WP_LANG_DIR;
404 }
405 }
406