Skip to content

Commit 17e338a

Browse files
authored
test: add unit tests for Turkish locale placeholder validation (#29951)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> Crowdin sometimes emits Turkish copy as %{{name}} next to ICU-style {{…}}. In i18n-js, % + { starts a %{…} placeholder, so %{{…}} breaks interpolation and users see missing-placeholder text. Changes: locales/languages/tr.json — replace the old %%{placeholder} pattern with {{placeholder}}% where values are plain numbers (aligned with English). Keep perps.order.leverage_modal.liquidation_warning as {{percentage}} only (no extra % in the string), because the app already passes a value that includes %. Tests: locales/tr.placeholders.test.js walks every string in tr.json and fails if any value contains %{{ or %%{, so CI catches bad Crowdin exports quickly. ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: changes are limited to Turkish translation strings and a Jest test that validates placeholder formatting, with no runtime logic changes beyond string interpolation behavior. > > **Overview** > Fixes Turkish `tr.json` percent placeholders by replacing `%%{...}`/`%{{...}}`-style patterns with `{{...}}%` so interpolation works correctly. > > Adds `locales/tr.placeholders.test.js`, which recursively scans all leaf strings in `tr.json` and fails CI if any translation contains `%{{` or `%%{` to catch bad Crowdin exports early. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1cb8b9c. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent cdcabc7 commit 17e338a

2 files changed

Lines changed: 82 additions & 29 deletions

File tree

locales/languages/tr.json

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,7 +1134,7 @@
11341134
"edit_button": "Düzenle"
11351135
},
11361136
"stop_loss_prompt": {
1137-
"near_liquidation_title": "Likidasyona yalnızca %%{distance} uzaktasınız",
1137+
"near_liquidation_title": "Likidasyona yalnızca {{distance}}% uzaktasınız",
11381138
"near_liquidation_subtitle": "Pozisyonunuzun riskini azaltmak için marj ekleyin",
11391139
"add_margin_button": "Ekle",
11401140
"protect_losses_title": "Daha fazla zarara karşı korunun",
@@ -1386,7 +1386,7 @@
13861386
"current_price": "Mevcut fiyat",
13871387
"liquidation_price": "Likidasyon fiyatı",
13881388
"liquidation_distance": "Likidasyon mesafesi",
1389-
"liquidation_warning": "Fiyat %%{percentage} {{direction}} yaşarsa pozisyonunuz likidasyona uğrar",
1389+
"liquidation_warning": "Fiyat {{percentage}} {{direction}} yaşarsa pozisyonunuz likidasyona uğrar",
13901390
"drops": "düşüş",
13911391
"rises": "yükseliş",
13921392
"set_leverage": "{{leverage}} kaldıraç ayarla"
@@ -1816,7 +1816,7 @@
18161816
"provider_fee": "Sağlayıcı ücreti",
18171817
"bridge_fee": "Köprü ücreti",
18181818
"total": "Toplam ücretler",
1819-
"discount_message": "MetaMask Ödülleri ile %%{percentage} tasarruf ediyorsunuz."
1819+
"discount_message": "MetaMask Ödülleri ile {{percentage}}% tasarruf ediyorsunuz."
18201820
},
18211821
"closing_fees": {
18221822
"title": "Kapatma ücretleri",
@@ -2515,7 +2515,7 @@
25152515
"slippage": "Kayma",
25162516
"price_details": "Fiyat bilgileri",
25172517
"prediction_order": "Tahmin emri",
2518-
"prediction_order_description": "Her biri {{price}} fiyatta ~{{count}} sözleşme. Son tutar, emir defteri kullanılabilirliğe göre değişiklik gösterebilir (%%{slippage} orana kadar).",
2518+
"prediction_order_description": "Her biri {{price}} fiyatta ~{{count}} sözleşme. Son tutar, emir defteri kullanılabilirliğe göre değişiklik gösterebilir ({{slippage}}% orana kadar).",
25192519
"metamask_fee_description": "Bu tahmin işlemine yönelik hizmet ücreti",
25202520
"exchange_fee": "Borsa ücreti",
25212521
"exchange_fee_description": "Borsaya veya piyasaya ödenen ücret",
@@ -5735,8 +5735,8 @@
57355735
"included": "dahil",
57365736
"max_gas_fee": "Maks. gaz ücreti",
57375737
"edit": "Düzenle",
5738-
"quotes_include_fee": "%%{fee} MetaMask ücreti tekliflere dahildir",
5739-
"quotes_include_gas_and_metamask_fee": "Gaz ve %%{fee} MetaMask ücreti teklife dahildir",
5738+
"quotes_include_fee": "{{fee}}% MetaMask ücreti tekliflere dahildir",
5739+
"quotes_include_gas_and_metamask_fee": "Gaz ve {{fee}}% MetaMask ücreti teklife dahildir",
57405740
"tap_to_swap": "Takas işlemi gerçekleştirmek için dokunun",
57415741
"swipe_to_swap": "Swap gerçekleştirmek için kaydır",
57425742
"swipe_to": "Swap gerçekleştirmek için",
@@ -6340,24 +6340,24 @@
63406340
},
63416341
"earn": {
63426342
"claimable_bonus_tooltip": "mUSD tuttuğunuz için kazandığınız yıllıklandırılmış bonus. Bonusunuzu Linea üzerinde günlük olarak alabilirsiniz.",
6343-
"earn_a_percentage_bonus": "%%{percentage} bonus kazanın",
6344-
"percentage_bonus": "%%{percentage} bonus",
6343+
"earn_a_percentage_bonus": "{{percentage}}% bonus kazanın",
6344+
"percentage_bonus": "{{percentage}}% bonus",
63456345
"claimable_bonus": "Alınabilir bonus",
63466346
"claim_bonus": "Bonusu al",
63476347
"claim_bonus_with_fiat": "{{amount}} al",
63486348
"claim_bonus_subtitle": "Bonus, {{networkName}} üzerinde ödenecektir.",
6349-
"percentage_bonus_on_linea": "Linea üzerinde %%{percentage} bonus",
6349+
"percentage_bonus_on_linea": "Linea üzerinde {{percentage}}% bonus",
63506350
"claim": "Al",
63516351
"sounds_good": "Kulağa hoş geliyor",
6352-
"claimable_bonus_tooltip_with_percentage": "mUSD tuttuğunuz için kazandığınız %%{percentage} yıllıklandırılmış bonus. Bonusunuzu Linea üzerinde günlük olarak alabilirsiniz.",
6352+
"claimable_bonus_tooltip_with_percentage": "mUSD tuttuğunuz için kazandığınız {{percentage}}% yıllıklandırılmış bonus. Bonusunuzu Linea üzerinde günlük olarak alabilirsiniz.",
63536353
"your_bonus": "Bonusunuz",
63546354
"estimated_annual_bonus": "Tahmini yıllık bonus",
63556355
"lifetime_bonus_claimed": "Toplam bonus alındı",
63566356
"accruing_next_bonus": "Sonraki biriken bonus",
63576357
"no_accruing_bonus": "Biriken bonus yok",
63586358
"claim_amount_bonus": "{{amount}}$ bonus al",
63596359
"your_bonus_tooltip_your_bonus": "Bonusunuz",
6360-
"your_bonus_tooltip_your_bonus_desc": ": Yalnızca mUSD tutarak kazandığınız %%{percentage} yıllıklandırılmış bonus. Bonusunuzu Linea üzerinde günlük olarak alabilirsiniz. ",
6360+
"your_bonus_tooltip_your_bonus_desc": ": Yalnızca mUSD tutarak kazandığınız {{percentage}}% yıllıklandırılmış bonus. Bonusunuzu Linea üzerinde günlük olarak alabilirsiniz. ",
63616361
"your_bonus_tooltip_annual_bonus": "Tahmini yıllık bonus: ",
63626362
"your_bonus_tooltip_annual_bonus_desc": "Mevcut bakiyenize ve oranınıza göre bir yıl içinde kazanabileceğiniz tahmini tutar. Oran değişkendir ve farklılık gösterebilir.",
63636363
"your_bonus_tooltip_lifetime_bonus": "Alınan toplam bonus: ",
@@ -6448,14 +6448,14 @@
64486448
"musd_conversion": {
64496449
"ok": "Tamam",
64506450
"continue": "Devam et",
6451-
"convert_and_get_percentage_bonus": "Dönüştür ve %%{percentage} al",
6451+
"convert_and_get_percentage_bonus": "Dönüştür ve {{percentage}}% al",
64526452
"your_musd": "mUSD bakiyeniz",
64536453
"balance_breakdown_title": "Ağa göre mUSD bakiyeleriniz",
64546454
"balance_amount": "{{amount}} mUSD",
64556455
"balance_amount_with_symbol": "{{amount}} {{symbol}}",
64566456
"balance_fiat_unavailable": "",
64576457
"convert_to_musd": "mUSD'ye dönüştür",
6458-
"get_a_percentage_musd_bonus": "%%{percentage} mUSD bonus al",
6458+
"get_a_percentage_musd_bonus": "{{percentage}}% mUSD bonus al",
64596459
"convert": "Dönüştür",
64606460
"fetching_quote": "Teklif alınıyor...",
64616461
"you_convert": "Dönüştürdüğünüz tutar",
@@ -6471,32 +6471,32 @@
64716471
"failed": "mUSD dönüştürme işlemi başarısız oldu"
64726472
},
64736473
"education": {
6474-
"heading": "STABİL KRİPTO PARALARDA\n%%{percentage} AL",
6475-
"description": "Stabil kripto paralarınızı mUSD'ye dönüştürün ve günlük olarak alabileceğiniz %%{percentage} oranına varan yıllıklandırılmış bonus kazanın.",
6474+
"heading": "STABİL KRİPTO PARALARDA\n{{percentage}}% AL",
6475+
"description": "Stabil kripto paralarınızı mUSD'ye dönüştürün ve günlük olarak alabileceğiniz {{percentage}}% oranına varan yıllıklandırılmış bonus kazanın.",
64766476
"terms_apply": "Şartlar uygulanır.",
64776477
"primary_button": "Başlarken",
64786478
"secondary_button": "Şimdi değil"
64796479
},
64806480
"buy_musd": "mUSD al",
64816481
"get_musd": "mUSD kazan",
6482-
"bonus_title": "Stabil kripto paranızda %%{percentage} bonus kazanın",
6483-
"bonus_description": "Stabil kripto paranızı mUSD'ye dönüştürün ve %%{percentage} oranında yıllıklandırılmış bonus alın.",
6482+
"bonus_title": "Stabil kripto paranızda {{percentage}}% bonus kazanın",
6483+
"bonus_description": "Stabil kripto paranızı mUSD'ye dönüştürün ve {{percentage}}% oranında yıllıklandırılmış bonus alın.",
64846484
"powered_by_relay": "Destekleyen Relay",
64856485
"max": "Maksimum",
64866486
"quick_convert_button": "Dönüştür",
64876487
"learn_more": "Daha fazla bilgi edin",
64886488
"tooltip_title": "mUSD ile getiri elde et",
64896489
"tooltip_content": "USDC, USDT veya DAI'nizi MetaMask'in dolar destekli stabil kripto parası olan mUSD'ye dönüştürün. Tuttuğunuz her dolar için {{apy}} getiri elde edin.",
64906490
"quick_convert": {
6491-
"title": "Dönüştür ve %%{percentage} al",
6492-
"subtitle": "Stabil kripto paralarınızı mUSD'ye dönüştürün ve günlük olarak alabileceğiniz %%{percentage} oranına varan yıllıklandırılmış bonus alın.",
6491+
"title": "Dönüştür ve {{percentage}}% al",
6492+
"subtitle": "Stabil kripto paralarınızı mUSD'ye dönüştürün ve günlük olarak alabileceğiniz {{percentage}}% oranına varan yıllıklandırılmış bonus alın.",
64936493
"inline_failed_message": "Dönüştürme işlemi başarısız oldu. Tekrar deneyin.",
64946494
"confirmation": {
64956495
"title": "Maksimumu dönüştür"
64966496
}
64976497
},
6498-
"percentage_bonus": "%%{percentage} bonus",
6499-
"claim_percentage_bonus": "%%{percentage} bonus al",
6498+
"percentage_bonus": "{{percentage}}% bonus",
6499+
"claim_percentage_bonus": "{{percentage}}% bonus al",
65006500
"rate": "Oran"
65016501
},
65026502
"bonus_claim": {
@@ -6519,7 +6519,7 @@
65196519
},
65206520
"money": {
65216521
"title": "Para",
6522-
"apy_label": "%%{percentage} Yıllık Bileşik Getiri",
6522+
"apy_label": "{{percentage}}% Yıllık Bileşik Getiri",
65236523
"apy_info_label": "Yıllık Yüzde Getiri (APY) bilgisi",
65246524
"onboarding": {
65256525
"step_progress": "Adım {{current}}/{{total}}",
@@ -6565,12 +6565,12 @@
65656565
"subtitle": "Paranızı dilediğiniz yerde harcayın.",
65666566
"virtual_card": "Sanal kart",
65676567
"metal_card": "Metal kart",
6568-
"cashback": "%%{percentage} para iadesi",
6568+
"cashback": "{{percentage}}% para iadesi",
65696569
"get_now": "Hemen alın",
65706570
"link_title": "MetaMask Card'ı bağla",
65716571
"link_subtitle": "Para bakiyenizi harcayın ve kazanın.",
65726572
"link_bullet_cashback": "%3'e kadar para iadesi",
6573-
"link_bullet_apy": "%%{apy} APY'ye varan",
6573+
"link_bullet_apy": "{{apy}}% APY'ye varan",
65746574
"link_card": "Kartı bağla"
65756575
},
65766576
"what_you_get": {
@@ -6834,7 +6834,7 @@
68346834
"you_could_earn_up_to": "Yılda",
68356835
"per_year_on_your_tokens": "kazanabilirsiniz",
68366836
"deposit": "Para Yatır",
6837-
"gas_cost_impact_warning": "Uyarı: İşlemin gaz maliyeti yatırdığınız paranın %%{percentOverDeposit} fazlası olacaktır.",
6837+
"gas_cost_impact_warning": "Uyarı: İşlemin gaz maliyeti yatırdığınız paranın {{percentOverDeposit}}% fazlası olacaktır.",
68386838
"earnings_history_title": "{{ticker}} kazançları",
68396839
"apr": "APR",
68406840
"interactive_chart": {
@@ -7202,7 +7202,7 @@
72027202
"title": "Köprü",
72037203
"submitting_transaction": "Gönderiliyor",
72047204
"fetching_quote": "Teklif alınıyor",
7205-
"fee_disclaimer": "%%{feePercentage} MetaMask ücreti dahildir.",
7205+
"fee_disclaimer": "{{feePercentage}}% MetaMask ücreti dahildir.",
72067206
"no_mm_fee": "MM ücreti yok",
72077207
"token_suspicious": "Şüpheli",
72087208
"token_malicious": "Kötü amaçlı",
@@ -7248,8 +7248,8 @@
72487248
"confirm": "Onayla",
72497249
"exceeding_upper_slippage_warning": "Yüksek kayma; bu durum istenmeyen bir takas ile sonuçlanabilir",
72507250
"exceeding_lower_slippage_warning": "Düşük kayma; bu durum istenmeyen bir takas ile sonuçlanabilir",
7251-
"exceeding_lower_slippage_error": "%%{value} değerin üzerinde olan bir değer girin",
7252-
"exceeding_upper_slippage_error": "%%{value} değerin üzerinde olan bir değer giremezsiniz",
7251+
"exceeding_lower_slippage_error": "{{value}}% değerin üzerinde olan bir değer girin",
7252+
"exceeding_upper_slippage_error": "{{value}}% değerin üzerinde olan bir değer giremezsiniz",
72537253
"custom": "Özel",
72547254
"invalid_recipient_address": "Geçersiz adres",
72557255
"select_quote": "Teklif Seç",
@@ -8984,7 +8984,7 @@
89848984
},
89858985
"cash_filled_state": {
89868986
"add": "Ekle",
8987-
"apy": "%%{percentage} Yıllık Bileşik Getiri"
8987+
"apy": "{{percentage}}% Yıllık Bileşik Getiri"
89888988
},
89898989
"tokens": "tokenlar",
89908990
"perpetuals": "Sürekli Vadeli İşlemler",

locales/tr.placeholders.test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import tr from './languages/tr.json';
2+
3+
/**
4+
* Recursively collects leaf string values from nested locale objects.
5+
*
6+
* @param {unknown} node - Current JSON node.
7+
* @param {string} path - Dot/bracket path for failure messages.
8+
* @param {Array<{ path: string; value: string }>} acc - Collected strings.
9+
*/
10+
function collectStrings(node, path, acc) {
11+
if (typeof node === 'string') {
12+
acc.push({ path, value: node });
13+
return;
14+
}
15+
if (Array.isArray(node)) {
16+
node.forEach((item, index) => {
17+
collectStrings(item, `${path}[${index}]`, acc);
18+
});
19+
return;
20+
}
21+
if (node !== null && typeof node === 'object') {
22+
for (const [key, child] of Object.entries(node)) {
23+
const nextPath = path ? `${path}.${key}` : key;
24+
collectStrings(child, nextPath, acc);
25+
}
26+
}
27+
}
28+
29+
describe('Turkish locale (tr.json)', () => {
30+
/** @type {() => Array<{ path: string; value: string }>} */
31+
function getAllLeafStrings() {
32+
/** @type {Array<{ path: string; value: string }>} */
33+
const strings = [];
34+
collectStrings(tr, '', strings);
35+
return strings;
36+
}
37+
38+
it('contains no %{{ substrings in any translation value', () => {
39+
const offenders = getAllLeafStrings().filter(({ value }) =>
40+
value.includes('%{{'),
41+
);
42+
43+
expect(offenders).toEqual([]);
44+
});
45+
46+
it('does not use %%{…} (literal % before placeholder); use {{…}}% like other locales', () => {
47+
const offenders = getAllLeafStrings().filter(({ value }) =>
48+
value.includes('%%{'),
49+
);
50+
51+
expect(offenders).toEqual([]);
52+
});
53+
});

0 commit comments

Comments
 (0)