์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŒ iOS ๊ตญ์ œํ™”์™€ NSLocalizedString์„ ํ™œ์šฉํ•œ ๋‹ค๊ตญ์–ด ์ง€์›

๐ŸŒ iOS ๊ตญ์ œํ™”์™€ NSLocalizedString์„ ํ™œ์šฉํ•œ ๋‹ค๊ตญ์–ด ์ง€์›

์ „ ์„ธ๊ณ„ ์‚ฌ์šฉ์ž์™€ ์†Œํ†ตํ•˜๋Š” ์•ฑ ๋งŒ๋“ค๊ธฐ ๐Ÿš€

์•ˆ๋…•ํ•˜์„ธ์š” ํ•œ๊ตญ์–ด ๐Ÿ‡ฐ๐Ÿ‡ท Hello English ๐Ÿ‡บ๐Ÿ‡ธ ใ“ใ‚“ใซใกใฏ ๆ—ฅๆœฌ่ชž ๐Ÿ‡ฏ๐Ÿ‡ต

๐ŸŽฏ ์™œ ๊ตญ์ œํ™”๊ฐ€ ์ค‘์š”ํ• ๊นŒ?

์•ˆ๋…•! ์˜ค๋Š˜์€ iOS ์•ฑ ๊ฐœ๋ฐœ์—์„œ ์ •๋ง ์ค‘์š”ํ•œ ์ฃผ์ œ์ธ ๊ตญ์ œํ™”(Internationalization, ์ค„์—ฌ์„œ i18n)์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. ๐Ÿ˜Š

ํ˜น์‹œ ์•ฑ์„ ๋งŒ๋“ค๋ฉด์„œ "์šฐ๋ฆฌ ์•ฑ์„ ์ „ ์„ธ๊ณ„ ์‚ฌ๋žŒ๋“ค์ด ์‚ฌ์šฉํ•˜๋ฉด ์ข‹๊ฒ ๋‹ค"๋Š” ์ƒ๊ฐ ํ•ด๋ณธ ์  ์žˆ์–ด? ์•„๋‹ˆ๋ฉด "ํ•œ๊ตญ์–ด๋กœ๋งŒ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ ์™ธ๊ตญ์ธ ์นœ๊ตฌ๊ฐ€ ์“ฐ๊ธฐ ํž˜๋“ค๋‹ค๊ณ  ํ•˜๋”๋ผ"๋Š” ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์€ ์ ์€? ๋ฐ”๋กœ ์ด๋Ÿด ๋•Œ ํ•„์š”ํ•œ ๊ฒŒ ๊ตญ์ œํ™”์•ผ! ๐ŸŒ

๊ตญ์ œํ™”๋Š” ๋‹จ์ˆœํžˆ ์–ธ์–ด๋ฅผ ๋ฒˆ์—ญํ•˜๋Š” ๊ฒƒ ์ด์ƒ์˜ ์˜๋ฏธ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์–ด. ๋‚ ์งœ ํ˜•์‹, ํ†ตํ™” ํ‘œ์‹œ, ์ˆซ์ž ํฌ๋งท, ์‹ฌ์ง€์–ด ํ…์ŠคํŠธ๊ฐ€ ํ‘œ์‹œ๋˜๋Š” ๋ฐฉํ–ฅ(์•„๋ž์–ด๋Š” ์˜ค๋ฅธ์ชฝ์—์„œ ์™ผ์ชฝ์œผ๋กœ!)๊นŒ์ง€ ๋ชจ๋‘ ๊ณ ๋ คํ•ด์•ผ ํ•˜๊ฑฐ๋“ . ๐Ÿค”

๐Ÿ’ก ๊ตญ์ œํ™” vs ์ง€์—ญํ™” ๊ตญ์ œํ™”(i18n): ์•ฑ์„ ์—ฌ๋Ÿฌ ์–ธ์–ด์™€ ์ง€์—ญ์— ์‰ฝ๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„ํ•˜๋Š” ๊ณผ์ •
์ง€์—ญํ™”(l10n): ํŠน์ • ์–ธ์–ด์™€ ์ง€์—ญ์— ๋งž๊ฒŒ ์‹ค์ œ๋กœ ๋ฒˆ์—ญํ•˜๊ณ  ์กฐ์ •ํ•˜๋Š” ๊ณผ์ •

์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, ๊ตญ์ œํ™”๋Š” "์ค€๋น„ ์ž‘์—…"์ด๊ณ  ์ง€์—ญํ™”๋Š” "์‹ค์ œ ์ ์šฉ"์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋ผ! ๐Ÿ“

App Store ํ†ต๊ณ„๋ฅผ ๋ณด๋ฉด ์ •๋ง ๋†€๋ผ์šด ์‚ฌ์‹ค์„ ์•Œ ์ˆ˜ ์žˆ์–ด. ๋‹ค๊ตญ์–ด๋ฅผ ์ง€์›ํ•˜๋Š” ์•ฑ์€ ๊ทธ๋ ‡์ง€ ์•Š์€ ์•ฑ๋ณด๋‹ค ํ‰๊ท ์ ์œผ๋กœ 128% ๋” ๋งŽ์€ ๋‹ค์šด๋กœ๋“œ๋ฅผ ๊ธฐ๋กํ•œ๋‹ค๊ณ  ํ•ด. ๐Ÿ˜ฒ

ํŠนํžˆ ์žฌ๋Šฅ๋„ท(https://www.jaenung.net) ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์•ฑ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•  ๋•Œ, ๊ตญ์ œํ™” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ํ›จ์”ฌ ๋” ๋†’์€ ํ‰๊ฐ€๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด. ๊ธ€๋กœ๋ฒŒ ์‹œ์žฅ์„ ํƒ€๊ฒŸ์œผ๋กœ ํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ๋“ค์—๊ฒŒ๋Š” ํ•„์ˆ˜ ๊ธฐ๋Šฅ์ด๋‹ˆ๊นŒ! ๐Ÿ’ผ

๐Ÿ›  NSLocalizedString์˜ ๊ธฐ๋ณธ ๊ฐœ๋…

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ Swift์—์„œ ๋‹ค๊ตญ์–ด๋ฅผ ์ง€์›ํ•˜๋Š” ํ•ต์‹ฌ ๋„๊ตฌ์ธ NSLocalizedString์— ๋Œ€ํ•ด ์•Œ์•„๋ณผ๊นŒ? ๐Ÿ”

NSLocalizedString์€ Foundation ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์ œ๊ณตํ•˜๋Š” ๋งคํฌ๋กœ ํ•จ์ˆ˜์•ผ. ์ด ์นœ๊ตฌ์˜ ์—ญํ• ์€ ์•„์ฃผ ๊ฐ„๋‹จํ•ด. ์ฝ”๋“œ์—์„œ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด ๋Œ€์‹ , ์‚ฌ์šฉ์ž์˜ ์–ธ์–ด ์„ค์ •์— ๋งž๋Š” ๋ฒˆ์—ญ๋œ ๋ฌธ์ž์—ด์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ฑฐ์ง€! โœจ

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•
// ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ํ˜•ํƒœ
let greeting = NSLocalizedString("greeting", comment: "์ธ์‚ฌ๋ง")

// ์‹ค์ œ ์‚ฌ์šฉ ์˜ˆ์‹œ
label.text = NSLocalizedString("welcome_message", comment: "ํ™˜์˜ ๋ฉ”์‹œ์ง€")
button.setTitle(NSLocalizedString("login_button", comment: "๋กœ๊ทธ์ธ ๋ฒ„ํŠผ"), for: .normal)

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ํฌ์ธํŠธ๊ฐ€ ๋ช‡ ๊ฐ€์ง€ ์žˆ์–ด! ๐Ÿ‘‡

1. Key ๊ฐ’: ์ฒซ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” "ํ‚ค(key)" ์—ญํ• ์„ ํ•ด. ์ด ํ‚ค๋ฅผ ํ†ตํ•ด ์‹ค์ œ ๋ฒˆ์—ญ๋œ ๋ฌธ์ž์—ด์„ ์ฐพ์•„์˜ค๋Š” ๊ฑฐ์•ผ.

2. Comment: ๋‘ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๋ฒˆ์—ญ๊ฐ€๋“ค์„ ์œ„ํ•œ ์„ค๋ช…์ด์•ผ. "์ด ๋ฌธ์ž์—ด์ด ์–ด๋””์— ์“ฐ์ด๋Š”์ง€" ์•Œ๋ ค์ฃผ๋Š” ๊ฑฐ์ง€. ๋‚˜์ค‘์— ๋ฒˆ์—ญ ์ž‘์—…ํ•  ๋•Œ ์—„์ฒญ ์œ ์šฉํ•ด! ๐Ÿ“‹

3. ์ž๋™ ์–ธ์–ด ๊ฐ์ง€: ์‚ฌ์šฉ์ž์˜ ๊ธฐ๊ธฐ ์–ธ์–ด ์„ค์ •์„ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•ด์„œ ์ ์ ˆํ•œ ๋ฒˆ์—ญ์„ ๋ณด์—ฌ์ค˜. ๊ฐœ๋ฐœ์ž๊ฐ€ ๋”ฐ๋กœ ์–ธ์–ด๋ฅผ ์ฒดํฌํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค๋Š” ๊ฒŒ ์ •๋ง ํŽธ๋ฆฌํ•˜์ง€! ๐ŸŽ‰

๐Ÿ’š Pro Tip Comment๋Š” ์ ˆ๋Œ€ ์ƒ๋žตํ•˜์ง€ ๋งˆ! "๋‚˜์ค‘์— ๋‚ด๊ฐ€ ์•Œ์•„์„œ ํ•˜์ง€ ๋ญ~"๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ์•ˆ ๋ผ. 6๊ฐœ์›” ๋’ค์˜ ๋„ˆ๋Š” ์ด ๋ฌธ์ž์—ด์ด ์–ด๋””์— ์“ฐ์˜€๋Š”์ง€ ๊ธฐ์–ต ๋ชป ํ•  ๊ฑฐ์•ผ. ๐Ÿ˜… ๊ทธ๋ฆฌ๊ณ  ๋‹ค๋ฅธ ์‚ฌ๋žŒ๊ณผ ํ˜‘์—…ํ•  ๋•Œ๋Š” ๋”๋”์šฑ ์ค‘์š”ํ•ด!

๐Ÿ“ฆ NSLocalizedString์˜ ๋‹ค์–‘ํ•œ ๋ณ€ํ˜•๋“ค

NSLocalizedString์—๋Š” ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ณ€ํ˜• ํ•จ์ˆ˜๋“ค์ด ์žˆ์–ด. ์ƒํ™ฉ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๊ฑธ ์„ ํƒํ•ด์„œ ์“ฐ๋ฉด ๋ผ! ๐ŸŽจ

๋‹ค์–‘ํ•œ NSLocalizedString ํ•จ์ˆ˜๋“ค
// 1. ๊ธฐ๋ณธ NSLocalizedString
NSLocalizedString("key", comment: "์„ค๋ช…")

// 2. ํŠน์ • ํ…Œ์ด๋ธ” ํŒŒ์ผ ์ง€์ •
NSLocalizedString("key", tableName: "CustomStrings", comment: "์„ค๋ช…")

// 3. ํŠน์ • ๋ฒˆ๋“ค ์ง€์ •
NSLocalizedString("key", bundle: .main, comment: "์„ค๋ช…")

// 4. ๋ชจ๋“  ์˜ต์…˜ ์ง€์ •
NSLocalizedString("key", 
                  tableName: "Localizable", 
                  bundle: .main, 
                  value: "๊ธฐ๋ณธ๊ฐ’", 
                  comment: "์„ค๋ช…")

// 5. ๋ณต์ˆ˜ํ˜• ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ํŠน๋ณ„ํ•œ ํ•จ์ˆ˜
let format = NSLocalizedString("items_count", comment: "์•„์ดํ…œ ๊ฐœ์ˆ˜")
let message = String.localizedStringWithFormat(format, itemCount)

๊ฐ๊ฐ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ญ˜ ์˜๋ฏธํ•˜๋Š”์ง€ ์ž์„ธํžˆ ๋ณผ๊นŒ? ๐Ÿ”Ž

ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค๋ช… ๊ธฐ๋ณธ๊ฐ’
key ๋ฒˆ์—ญ ๋ฌธ์ž์—ด์„ ์ฐพ๊ธฐ ์œ„ํ•œ ๊ณ ์œ  ์‹๋ณ„์ž ํ•„์ˆ˜
tableName ๋ฌธ์ž์—ด์ด ์ €์žฅ๋œ .strings ํŒŒ์ผ ์ด๋ฆ„ "Localizable"
bundle ๋ฌธ์ž์—ด ํŒŒ์ผ์ด ์žˆ๋Š” ๋ฒˆ๋“ค .main
value ๋ฒˆ์—ญ์„ ์ฐพ์ง€ ๋ชปํ–ˆ์„ ๋•Œ ์‚ฌ์šฉํ•  ๊ธฐ๋ณธ๊ฐ’ key ๊ฐ’
comment ๋ฒˆ์—ญ๊ฐ€๋ฅผ ์œ„ํ•œ ์„ค๋ช… ํ•„์ˆ˜

๐Ÿ“ Localizable.strings ํŒŒ์ผ ๋งŒ๋“ค๊ธฐ

์ž, ์ด์ œ ์‹ค์ œ๋กœ ๋ฒˆ์—ญ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด๋ณผ ์‹œ๊ฐ„์ด์•ผ! NSLocalizedString์ด ์ฐธ์กฐํ•˜๋Š” ์‹ค์ œ ๋ฒˆ์—ญ ๋ฐ์ดํ„ฐ๋Š” Localizable.strings ํŒŒ์ผ์— ์ €์žฅ๋ผ. ๐Ÿ“„

์ด ํŒŒ์ผ์„ ๋งŒ๋“œ๋Š” ๊ณผ์ •์„ ๋‹จ๊ณ„๋ณ„๋กœ ์ž์„ธํžˆ ์•Œ์•„๋ณด์ž! ๐Ÿšถโ€โ™‚๏ธ

Step 1: ์ƒˆ ํŒŒ์ผ ์ƒ์„ฑ ๐Ÿ†•

Xcode์—์„œ File โ†’ New โ†’ File... ์„ ํƒ
Resource ์„น์…˜์—์„œ "Strings File" ์„ ํƒ
ํŒŒ์ผ ์ด๋ฆ„์„ ์ •ํ™•ํžˆ "Localizable"๋กœ ์ž…๋ ฅ (๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„!)
ํ”„๋กœ์ ํŠธ์˜ ์ ์ ˆํ•œ ์œ„์น˜์— ์ €์žฅ
Step 2: ์–ธ์–ด ์ถ”๊ฐ€ ๐ŸŒ

ํ”„๋กœ์ ํŠธ ๋„ค๋น„๊ฒŒ์ดํ„ฐ์—์„œ ํ”„๋กœ์ ํŠธ ํŒŒ์ผ ์„ ํƒ
PROJECT ์„น์…˜์—์„œ ํ”„๋กœ์ ํŠธ ์„ ํƒ
Info ํƒญ์—์„œ Localizations ์„น์…˜ ์ฐพ๊ธฐ
+ ๋ฒ„ํŠผ ํด๋ฆญํ•ด์„œ ์›ํ•˜๋Š” ์–ธ์–ด ์ถ”๊ฐ€ (์˜ˆ: Korean, Japanese, Spanish ๋“ฑ)
Step 3: Localizable.strings ํŒŒ์ผ ์ง€์—ญํ™” ๐Ÿ”ง

Localizable.strings ํŒŒ์ผ ์„ ํƒ
์˜ค๋ฅธ์ชฝ ์ธ์ŠคํŽ™ํ„ฐ์—์„œ "Localize..." ๋ฒ„ํŠผ ํด๋ฆญ
Base ์–ธ์–ด ์„ ํƒ ํ›„ Localize ํด๋ฆญ
์ธ์ŠคํŽ™ํ„ฐ์—์„œ ์ถ”๊ฐ€ํ•œ ๋ชจ๋“  ์–ธ์–ด ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ

์ด ๊ณผ์ •์„ ๊ฑฐ์น˜๋ฉด ํ”„๋กœ์ ํŠธ์— ๊ฐ ์–ธ์–ด๋ณ„๋กœ Localizable.strings ํŒŒ์ผ์ด ์ƒ์„ฑ๋ผ! ์˜ˆ๋ฅผ ๋“ค์–ด: ๐Ÿ“‚

- Localizable.strings (Base/English)
- Localizable.strings (Korean)
- Localizable.strings (Japanese)
- Localizable.strings (Spanish)

โœ๏ธ ๋ฒˆ์—ญ ๋ฌธ์ž์—ด ์ž‘์„ฑํ•˜๊ธฐ

์ด์ œ ๊ฐ ํŒŒ์ผ์— ์‹ค์ œ ๋ฒˆ์—ญ์„ ์ž‘์„ฑํ•ด์•ผ ํ•ด. ํ˜•์‹์€ ์•„์ฃผ ๊ฐ„๋‹จํ•ด! ๐ŸŽฏ

Localizable.strings (English)
/* ์ธ์‚ฌ๋ง */
"greeting" = "Hello";

/* ํ™˜์˜ ๋ฉ”์‹œ์ง€ */
"welcome_message" = "Welcome to our app!";

/* ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ */
"login_button" = "Login";

/* ์„ค์ • ํ™”๋ฉด ์ œ๋ชฉ */
"settings_title" = "Settings";

/* ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ */
"notification_new_message" = "You have a new message";

/* ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ */
"error_network" = "Network connection failed. Please try again.";
Localizable.strings (Korean)
/* ์ธ์‚ฌ๋ง */
"greeting" = "์•ˆ๋…•ํ•˜์„ธ์š”";

/* ํ™˜์˜ ๋ฉ”์‹œ์ง€ */
"welcome_message" = "์šฐ๋ฆฌ ์•ฑ์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!";

/* ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ */
"login_button" = "๋กœ๊ทธ์ธ";

/* ์„ค์ • ํ™”๋ฉด ์ œ๋ชฉ */
"settings_title" = "์„ค์ •";

/* ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ */
"notification_new_message" = "์ƒˆ๋กœ์šด ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค";

/* ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ */
"error_network" = "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.";
Localizable.strings (Japanese)
/* ์ธ์‚ฌ๋ง */
"greeting" = "ใ“ใ‚“ใซใกใฏ";

/* ํ™˜์˜ ๋ฉ”์‹œ์ง€ */
"welcome_message" = "็งใŸใกใฎใ‚ขใƒ—ใƒชใธใ‚ˆใ†ใ“ใ!";

/* ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ */
"login_button" = "ใƒญใ‚ฐใ‚คใƒณ";

/* ์„ค์ • ํ™”๋ฉด ์ œ๋ชฉ */
"settings_title" = "่จญๅฎš";

/* ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ */
"notification_new_message" = "ๆ–ฐใ—ใ„ใƒกใƒƒใ‚ปใƒผใ‚ธใŒใ‚ใ‚Šใพใ™";

/* ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ */
"error_network" = "ใƒใƒƒใƒˆใƒฏใƒผใ‚ฏๆŽฅ็ถšใซๅคฑๆ•—ใ—ใพใ—ใŸใ€‚ใ‚‚ใ†ไธ€ๅบฆใŠ่ฉฆใ—ใใ ใ•ใ„ใ€‚";
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ 1. ๊ฐ ์ค„์€ ๋ฐ˜๋“œ์‹œ ์„ธ๋ฏธ์ฝœ๋ก (;)์œผ๋กœ ๋๋‚˜์•ผ ํ•ด!
2. ํ‚ค ๊ฐ’์€ ๋ชจ๋“  ์–ธ์–ด ํŒŒ์ผ์—์„œ ๋™์ผํ•ด์•ผ ํ•ด
3. ๋”ฐ์˜ดํ‘œ๋Š” ๋ฐ˜๋“œ์‹œ ํฐ๋”ฐ์˜ดํ‘œ(")๋ฅผ ์‚ฌ์šฉํ•ด
4. ์ฃผ์„์€ /* */ ํ˜•ํƒœ๋กœ ์ž‘์„ฑํ•˜๋ฉฐ, ๋ฒˆ์—ญ๊ฐ€๋“ค์—๊ฒŒ ํฐ ๋„์›€์ด ๋ผ
5. ํŒŒ์ผ ์ธ์ฝ”๋”ฉ์€ UTF-8๋กœ ์„ค์ •ํ•ด์•ผ ํ•ด (ํŠน์ˆ˜๋ฌธ์ž ๊นจ์ง ๋ฐฉ์ง€)

๐ŸŽจ ์‹ค์ „ ์˜ˆ์ œ: ๋‹ค๊ตญ์–ด ์•ฑ ๋งŒ๋“ค๊ธฐ

์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ๋ฐฐ์› ์œผ๋‹ˆ, ์ด์ œ ์‹ค์ œ๋กœ ๋™์ž‘ํ•˜๋Š” ์•ฑ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๊ฐ„๋‹จํ•œ ํ”„๋กœํ•„ ํ™”๋ฉด์„ ๋‹ค๊ตญ์–ด๋กœ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ. ๐Ÿ˜Ž

๐Ÿ— ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์„ค์ •

ViewController.swift
import UIKit

class ProfileViewController: UIViewController {
    
    // MARK: - UI Components
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 24, weight: .bold)
        label.textAlignment = .center
        return label
    }()
    
    private let nameLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 18)
        return label
    }()
    
    private let emailLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 18)
        return label
    }()
    
    private let bioLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 16)
        label.numberOfLines = 0
        return label
    }()
    
    private let editButton: UIButton = {
        let button = UIButton(type: .system)
        button.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
        button.backgroundColor = .systemBlue
        button.setTitleColor(.white, for: .normal)
        button.layer.cornerRadius = 8
        return button
    }()
    
    private let logoutButton: UIButton = {
        let button = UIButton(type: .system)
        button.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
        button.backgroundColor = .systemRed
        button.setTitleColor(.white, for: .normal)
        button.layer.cornerRadius = 8
        return button
    }()
    
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        configureLocalizedStrings()
    }
    
    // MARK: - Setup
    private func setupUI() {
        view.backgroundColor = .systemBackground
        
        [titleLabel, nameLabel, emailLabel, bioLabel, 
         editButton, logoutButton].forEach {
            view.addSubview($0)
            $0.translatesAutoresizingMaskIntoConstraints = false
        }
        
        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            
            nameLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 40),
            nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            
            emailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 20),
            emailLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            emailLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            
            bioLabel.topAnchor.constraint(equalTo: emailLabel.bottomAnchor, constant: 20),
            bioLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            bioLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            
            editButton.topAnchor.constraint(equalTo: bioLabel.bottomAnchor, constant: 40),
            editButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            editButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            editButton.heightAnchor.constraint(equalToConstant: 50),
            
            logoutButton.topAnchor.constraint(equalTo: editButton.bottomAnchor, constant: 15),
            logoutButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            logoutButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            logoutButton.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
    
    private func configureLocalizedStrings() {
        // ํ™”๋ฉด ์ œ๋ชฉ
        titleLabel.text = NSLocalizedString("profile_title", 
                                           comment: "ํ”„๋กœํ•„ ํ™”๋ฉด ์ œ๋ชฉ")
        
        // ์‚ฌ์šฉ์ž ์ •๋ณด ๋ ˆ์ด๋ธ”
        let nameFormat = NSLocalizedString("profile_name_label", 
                                          comment: "์ด๋ฆ„ ๋ ˆ์ด๋ธ”")
        nameLabel.text = String(format: nameFormat, "๊น€์ฒ ์ˆ˜")
        
        let emailFormat = NSLocalizedString("profile_email_label", 
                                           comment: "์ด๋ฉ”์ผ ๋ ˆ์ด๋ธ”")
        emailLabel.text = String(format: emailFormat, "chulsoo@example.com")
        
        // ์ž๊ธฐ์†Œ๊ฐœ
        bioLabel.text = NSLocalizedString("profile_bio_placeholder", 
                                         comment: "์ž๊ธฐ์†Œ๊ฐœ ๊ธฐ๋ณธ ํ…์ŠคํŠธ")
        
        // ๋ฒ„ํŠผ ํ…์ŠคํŠธ
        editButton.setTitle(NSLocalizedString("profile_edit_button", 
                                             comment: "ํ”„๋กœํ•„ ์ˆ˜์ • ๋ฒ„ํŠผ"), 
                           for: .normal)
        
        logoutButton.setTitle(NSLocalizedString("profile_logout_button", 
                                               comment: "๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ"), 
                             for: .normal)
    }
}

๐Ÿ“ ๋ฒˆ์—ญ ํŒŒ์ผ ์ž‘์„ฑ

Localizable.strings (English)
/* ํ”„๋กœํ•„ ํ™”๋ฉด */
"profile_title" = "My Profile";
"profile_name_label" = "Name: %@";
"profile_email_label" = "Email: %@";
"profile_bio_placeholder" = "Tell us about yourself...";
"profile_edit_button" = "Edit Profile";
"profile_logout_button" = "Logout";

/* ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ */
"profile_update_success" = "Profile updated successfully!";
"profile_update_failed" = "Failed to update profile. Please try again.";
"logout_confirm_title" = "Logout";
"logout_confirm_message" = "Are you sure you want to logout?";
"confirm_yes" = "Yes";
"confirm_no" = "No";
Localizable.strings (Korean)
/* ํ”„๋กœํ•„ ํ™”๋ฉด */
"profile_title" = "๋‚ด ํ”„๋กœํ•„";
"profile_name_label" = "์ด๋ฆ„: %@";
"profile_email_label" = "์ด๋ฉ”์ผ: %@";
"profile_bio_placeholder" = "์ž์‹ ์— ๋Œ€ํ•ด ์†Œ๊ฐœํ•ด์ฃผ์„ธ์š”...";
"profile_edit_button" = "ํ”„๋กœํ•„ ์ˆ˜์ •";
"profile_logout_button" = "๋กœ๊ทธ์•„์›ƒ";

/* ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ */
"profile_update_success" = "ํ”„๋กœํ•„์ด ์„ฑ๊ณต์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!";
"profile_update_failed" = "ํ”„๋กœํ•„ ์—…๋ฐ์ดํŠธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.";
"logout_confirm_title" = "๋กœ๊ทธ์•„์›ƒ";
"logout_confirm_message" = "์ •๋ง ๋กœ๊ทธ์•„์›ƒํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?";
"confirm_yes" = "์˜ˆ";
"confirm_no" = "์•„๋‹ˆ์˜ค";
Localizable.strings (Japanese)
/* ํ”„๋กœํ•„ ํ™”๋ฉด */
"profile_title" = "ใƒžใ‚คใƒ—ใƒญใƒ•ใ‚ฃใƒผใƒซ";
"profile_name_label" = "ๅๅ‰: %@";
"profile_email_label" = "ใƒกใƒผใƒซ: %@";
"profile_bio_placeholder" = "่‡ชๅทฑ็ดนไป‹ใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„...";
"profile_edit_button" = "ใƒ—ใƒญใƒ•ใ‚ฃใƒผใƒซ็ทจ้›†";
"profile_logout_button" = "ใƒญใ‚ฐใ‚ขใ‚ฆใƒˆ";

/* ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ */
"profile_update_success" = "ใƒ—ใƒญใƒ•ใ‚ฃใƒผใƒซใŒๆญฃๅธธใซๆ›ดๆ–ฐใ•ใ‚Œใพใ—ใŸ!";
"profile_update_failed" = "ใƒ—ใƒญใƒ•ใ‚ฃใƒผใƒซใฎๆ›ดๆ–ฐใซๅคฑๆ•—ใ—ใพใ—ใŸใ€‚ใ‚‚ใ†ไธ€ๅบฆใŠ่ฉฆใ—ใใ ใ•ใ„ใ€‚";
"logout_confirm_title" = "ใƒญใ‚ฐใ‚ขใ‚ฆใƒˆ";
"logout_confirm_message" = "ๆœฌๅฝ“ใซใƒญใ‚ฐใ‚ขใ‚ฆใƒˆใ—ใพใ™ใ‹?";
"confirm_yes" = "ใฏใ„";
"confirm_no" = "ใ„ใ„ใˆ";

์—ฌ๊ธฐ์„œ ์ฃผ๋ชฉํ•  ์ ์€ %@ ๊ฐ™์€ ํฌ๋งท ์ง€์ •์ž๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค๋Š” ๊ฑฐ์•ผ! ์ด๊ฑด ๋™์ ์œผ๋กœ ๊ฐ’์„ ๋„ฃ์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํ”Œ๋ ˆ์ด์Šคํ™€๋”์•ผ. String(format:) ํ•จ์ˆ˜์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ์ •๋ง ์œ ์šฉํ•˜์ง€! ๐ŸŽฏ

๐Ÿ”ข ๋ณต์ˆ˜ํ˜•๊ณผ ๋ณ€์ˆ˜ ์ฒ˜๋ฆฌํ•˜๊ธฐ

๋‹ค๊ตญ์–ด ์ง€์›์—์„œ ๊นŒ๋‹ค๋กœ์šด ๋ถ€๋ถ„ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ ๋ณต์ˆ˜ํ˜• ์ฒ˜๋ฆฌ์•ผ. ์–ธ์–ด๋งˆ๋‹ค ๋ณต์ˆ˜ํ˜• ๊ทœ์น™์ด ์™„์ „ํžˆ ๋‹ค๋ฅด๊ฑฐ๋“ ! ๐Ÿ˜ต

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ:
- ์˜์–ด: "1 item", "2 items" (๋‹จ์ˆ˜/๋ณต์ˆ˜ 2๊ฐ€์ง€)
- ํ•œ๊ตญ์–ด: "1๊ฐœ", "2๊ฐœ" (๋ณต์ˆ˜ํ˜• ๊ตฌ๋ถ„ ์—†์Œ)
- ๋Ÿฌ์‹œ์•„์–ด: 1, 21, 31... / 2-4, 22-24... / 5-20, 25-30... (3๊ฐ€์ง€ ํ˜•ํƒœ!)
- ์•„๋ž์–ด: 0๊ฐœ, 1๊ฐœ, 2๊ฐœ, 3-10๊ฐœ, 11-99๊ฐœ, 100๊ฐœ ์ด์ƒ (6๊ฐ€์ง€ ํ˜•ํƒœ!!!) ๐Ÿ˜ฑ

๐Ÿ“Š Stringsdict ํŒŒ์ผ ํ™œ์šฉํ•˜๊ธฐ

iOS๋Š” ์ด๋Ÿฐ ๋ณต์žกํ•œ ๋ณต์ˆ˜ํ˜• ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด .stringsdict ํŒŒ์ผ์„ ์ œ๊ณตํ•ด. ์ด๊ฑด XML ํ˜•์‹์˜ ํŒŒ์ผ์ธ๋ฐ, ๋ณต์ˆ˜ํ˜• ๊ทœ์น™์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ“„

Stringsdict ํŒŒ์ผ ์ƒ์„ฑํ•˜๊ธฐ ๐Ÿ†•

File โ†’ New โ†’ File... ์„ ํƒ
Resource ์„น์…˜์—์„œ "Stringsdict File" ์„ ํƒ
ํŒŒ์ผ ์ด๋ฆ„์„ "Localizable"๋กœ ์ž…๋ ฅ
Localizable.strings์™€ ๋™์ผํ•˜๊ฒŒ ์ง€์—ญํ™” ์„ค์ •
Localizable.stringsdict (English)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>items_count</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@item_count@</string>
        <key>item_count</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>zero</key>
            <string>No items</string>
            <key>one</key>
            <string>1 item</string>
            <key>other</key>
            <string>%d items</string>
        </dict>
    </dict>
    
    <key>messages_count</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@message_count@</string>
        <key>message_count</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>zero</key>
            <string>No new messages</string>
            <key>one</key>
            <string>1 new message</string>
            <key>other</key>
            <string>%d new messages</string>
        </dict>
    </dict>
</dict>
</plist>
Localizable.stringsdict (Korean)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>items_count</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@item_count@</string>
        <key>item_count</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>other</key>
            <string>%d๊ฐœ์˜ ํ•ญ๋ชฉ</string>
        </dict>
    </dict>
    
    <key>messages_count</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@message_count@</string>
        <key>message_count</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>other</key>
            <string>์ƒˆ ๋ฉ”์‹œ์ง€ %d๊ฐœ</string>
        </dict>
    </dict>
</dict>
</plist>

๐Ÿ’ป ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ

๋ณต์ˆ˜ํ˜• ๋ฌธ์ž์—ด ์‚ฌ์šฉ ์˜ˆ์ œ
class ShoppingCartViewController: UIViewController {
    
    var itemCount: Int = 0 {
        didSet {
            updateItemCountLabel()
        }
    }
    
    private let itemCountLabel = UILabel()
    
    private func updateItemCountLabel() {
        // stringsdict ํŒŒ์ผ์˜ ๋ณต์ˆ˜ํ˜• ๊ทœ์น™์ด ์ž๋™์œผ๋กœ ์ ์šฉ๋จ!
        let format = NSLocalizedString("items_count", comment: "์žฅ๋ฐ”๊ตฌ๋‹ˆ ์•„์ดํ…œ ๊ฐœ์ˆ˜")
        itemCountLabel.text = String.localizedStringWithFormat(format, itemCount)
        
        // itemCount๊ฐ€ 0์ด๋ฉด: "No items" (์˜์–ด) / "0๊ฐœ์˜ ํ•ญ๋ชฉ" (ํ•œ๊ตญ์–ด)
        // itemCount๊ฐ€ 1์ด๋ฉด: "1 item" (์˜์–ด) / "1๊ฐœ์˜ ํ•ญ๋ชฉ" (ํ•œ๊ตญ์–ด)
        // itemCount๊ฐ€ 5์ด๋ฉด: "5 items" (์˜์–ด) / "5๊ฐœ์˜ ํ•ญ๋ชฉ" (ํ•œ๊ตญ์–ด)
    }
    
    private func showNewMessagesAlert(count: Int) {
        let format = NSLocalizedString("messages_count", comment: "์ƒˆ ๋ฉ”์‹œ์ง€ ๊ฐœ์ˆ˜")
        let message = String.localizedStringWithFormat(format, count)
        
        let alert = UIAlertController(title: NSLocalizedString("notification_title", 
                                                               comment: "์•Œ๋ฆผ ์ œ๋ชฉ"),
                                     message: message,
                                     preferredStyle: .alert)
        
        alert.addAction(UIAlertAction(title: NSLocalizedString("confirm_ok", 
                                                               comment: "ํ™•์ธ ๋ฒ„ํŠผ"),
                                     style: .default))
        
        present(alert, animated: true)
    }
}
๐Ÿ’š ๋ณต์ˆ˜ํ˜• ์ฒ˜๋ฆฌ ๊ฟ€ํŒ 1. ํ•œ๊ตญ์–ด๋Š” ๋ณต์ˆ˜ํ˜• ๊ตฌ๋ถ„์ด ์—†์–ด์„œ "other" ์ผ€์ด์Šค๋งŒ ์ •์˜ํ•ด๋„ ๋ผ
2. ์˜์–ด๋Š” "zero", "one", "other" ์„ธ ๊ฐ€์ง€๋งŒ ์žˆ์œผ๋ฉด ์ถฉ๋ถ„ํ•ด
3. ๋ณต์žกํ•œ ์–ธ์–ด(๋Ÿฌ์‹œ์•„์–ด, ์•„๋ž์–ด ๋“ฑ)๋ฅผ ์ง€์›ํ•  ๋•Œ๋Š” Apple์˜ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ๊ผญ ์ฐธ๊ณ ํ•ด!
4. String.localizedStringWithFormat๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๋ฐ˜๋“œ์‹œ NSLocalizedString์œผ๋กœ ํฌ๋งท์„ ๋จผ์ € ๊ฐ€์ ธ์™€์•ผ ํ•ด

๐ŸŽญ ์—ฌ๋Ÿฌ ๋ณ€์ˆ˜๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ

๋•Œ๋กœ๋Š” ํ•˜๋‚˜์˜ ๋ฌธ์ž์—ด์— ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋ณ€์ˆ˜๋ฅผ ๋„ฃ์–ด์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด "๊น€์ฒ ์ˆ˜๋‹˜์ด 5๊ฐœ์˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋ƒˆ์Šต๋‹ˆ๋‹ค" ๊ฐ™์€ ๊ฒฝ์šฐ ๋ง์ด์•ผ! ๐Ÿค”

์—ฌ๋Ÿฌ ๋ณ€์ˆ˜ ์ฒ˜๋ฆฌ ์˜ˆ์ œ
// Localizable.strings (English)
"message_from_user" = "%@ sent you %d messages";

// Localizable.strings (Korean)
"message_from_user" = "%@๋‹˜์ด ๋ฉ”์‹œ์ง€ %d๊ฐœ๋ฅผ ๋ณด๋ƒˆ์Šต๋‹ˆ๋‹ค";

// Swift ์ฝ”๋“œ
let userName = "๊น€์ฒ ์ˆ˜"
let messageCount = 5

let format = NSLocalizedString("message_from_user", 
                              comment: "์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ๋ฉ”์‹œ์ง€")
let message = String(format: format, userName, messageCount)

// ๊ฒฐ๊ณผ:
// ์˜์–ด: "๊น€์ฒ ์ˆ˜ sent you 5 messages"
// ํ•œ๊ตญ์–ด: "๊น€์ฒ ์ˆ˜๋‹˜์ด ๋ฉ”์‹œ์ง€ 5๊ฐœ๋ฅผ ๋ณด๋ƒˆ์Šต๋‹ˆ๋‹ค"

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๊ฑด ๋ณ€์ˆ˜์˜ ์ˆœ์„œ์•ผ! ์–ธ์–ด๋งˆ๋‹ค ๋ฌธ์žฅ ๊ตฌ์กฐ๊ฐ€ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์—, ๋ณ€์ˆ˜์˜ ์œ„์น˜๋„ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์–ด. ๊ทธ๋ž˜์„œ ํฌ๋งท ์ง€์ •์ž์— ์ˆœ์„œ๋ฅผ ๋ช…์‹œํ•  ์ˆ˜๋„ ์žˆ์–ด! ๐Ÿ”ข

๋ณ€์ˆ˜ ์ˆœ์„œ ์ง€์ •ํ•˜๊ธฐ
// ๋ณ€์ˆ˜ ์ˆœ์„œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •
// Localizable.strings (English)
"user_action" = "%1$@ deleted %2$d items from %3$@";

// Localizable.strings (Korean)  
"user_action" = "%1$@๋‹˜์ด %3$@์—์„œ %2$d๊ฐœ์˜ ํ•ญ๋ชฉ์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค";

// Swift ์ฝ”๋“œ
let format = NSLocalizedString("user_action", comment: "์‚ฌ์šฉ์ž ์•ก์…˜")
let message = String(format: format, "๊น€์ฒ ์ˆ˜", 3, "์žฅ๋ฐ”๊ตฌ๋‹ˆ")

// ์˜์–ด: "๊น€์ฒ ์ˆ˜ deleted 3 items from ์žฅ๋ฐ”๊ตฌ๋‹ˆ"
// ํ•œ๊ตญ์–ด: "๊น€์ฒ ์ˆ˜๋‹˜์ด ์žฅ๋ฐ”๊ตฌ๋‹ˆ์—์„œ 3๊ฐœ์˜ ํ•ญ๋ชฉ์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค"

%1$@, %2$d, %3$@ ์ด๋Ÿฐ ์‹์œผ๋กœ ์ˆซ์ž๋ฅผ ๋ถ™์ด๋ฉด ์ˆœ์„œ๋ฅผ ๋ช…์‹œํ•  ์ˆ˜ ์žˆ์–ด! ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์–ธ์–ด๋ณ„๋กœ ์ž์œ ๋กญ๊ฒŒ ๋ณ€์ˆ˜ ์ˆœ์„œ๋ฅผ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์ง€. ์ •๋ง ์œ ์šฉํ•˜์ง€? ๐Ÿ˜Š

๐ŸŽจ ์ด๋ฏธ์ง€์™€ ์—์…‹ ์ง€์—ญํ™”

ํ…์ŠคํŠธ๋งŒ ๋ฒˆ์—ญํ•œ๋‹ค๊ณ  ๋์ด ์•„๋‹ˆ์•ผ! ์ด๋ฏธ์ง€๋‚˜ ์•„์ด์ฝ˜์—๋„ ํ…์ŠคํŠธ๊ฐ€ ๋“ค์–ด๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์ž–์•„? ๐Ÿ–ผ

์˜ˆ๋ฅผ ๋“ค์–ด "Welcome" ํ…์ŠคํŠธ๊ฐ€ ๋“ค์–ด๊ฐ„ ๋ฐฐ๋„ˆ ์ด๋ฏธ์ง€๋‚˜, ํŠน์ • ๊ตญ๊ฐ€์˜ ๋ฌธํ™”๋ฅผ ๋ฐ˜์˜ํ•œ ์•„์ด์ฝ˜ ๊ฐ™์€ ๊ฑฐ ๋ง์ด์•ผ. ์ด๋Ÿฐ ๊ฒƒ๋“ค๋„ ์ง€์—ญํ™”๊ฐ€ ํ•„์š”ํ•ด! ๐ŸŒ

๐Ÿ“ฆ Asset Catalog์—์„œ ์ด๋ฏธ์ง€ ์ง€์—ญํ™”ํ•˜๊ธฐ

Step 1: Asset Catalog ์„ค์ • ๐ŸŽจ

Assets.xcassets ํŒŒ์ผ ์—ด๊ธฐ
์ง€์—ญํ™”ํ•  ์ด๋ฏธ์ง€ ์„ ํƒ
์˜ค๋ฅธ์ชฝ ์ธ์ŠคํŽ™ํ„ฐ์—์„œ "Localize..." ๋ฒ„ํŠผ ํด๋ฆญ
์ง€์›ํ•  ์–ธ์–ด๋“ค ์„ ํƒ
Step 2: ์–ธ์–ด๋ณ„ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ ๐Ÿ–ผ

๊ฐ ์–ธ์–ด๋ณ„๋กœ ์ ์ ˆํ•œ ์ด๋ฏธ์ง€๋ฅผ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ
์ด๋ฏธ์ง€ ์ด๋ฆ„์€ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€
์‹œ์Šคํ…œ์ด ์ž๋™์œผ๋กœ ์–ธ์–ด์— ๋งž๋Š” ์ด๋ฏธ์ง€๋ฅผ ์„ ํƒํ•จ
์ฝ”๋“œ์—์„œ ์ง€์—ญํ™”๋œ ์ด๋ฏธ์ง€ ์‚ฌ์šฉํ•˜๊ธฐ
class WelcomeViewController: UIViewController {
    
    private let bannerImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        // ์‹œ์Šคํ…œ์ด ์ž๋™์œผ๋กœ ํ˜„์žฌ ์–ธ์–ด์— ๋งž๋Š” ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œํ•จ
        imageView.image = UIImage(named: "welcome_banner")
        return imageView
    }()
    
    private let iconImageView: UIImageView = {
        let imageView = UIImageView()
        // ๊ตญ๊ฐ€๋ณ„๋กœ ๋‹ค๋ฅธ ์•„์ด์ฝ˜ ํ‘œ์‹œ
        imageView.image = UIImage(named: "country_icon")
        return imageView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        
        // ํ˜„์žฌ ์–ธ์–ด ํ™•์ธ (๋””๋ฒ„๊น…์šฉ)
        let currentLanguage = Locale.current.languageCode
        print("Current language: \(currentLanguage ?? "unknown")")
    }
}

๐ŸŽฏ SF Symbols ํ™œ์šฉํ•˜๊ธฐ

iOS 13๋ถ€ํ„ฐ ์ œ๊ณต๋˜๋Š” SF Symbols๋Š” ์ž๋™์œผ๋กœ RTL(Right-to-Left) ์–ธ์–ด๋ฅผ ์ง€์›ํ•ด! ์•„๋ž์–ด๋‚˜ ํžˆ๋ธŒ๋ฆฌ์–ด ๊ฐ™์€ ์–ธ์–ด์—์„œ ์•„์ด์ฝ˜์ด ์ž๋™์œผ๋กœ ๋’ค์ง‘ํ˜€์„œ ํ‘œ์‹œ๋˜๋Š” ๊ฑฐ์ง€. ์ •๋ง ํŽธ๋ฆฌํ•˜์ง€? ๐Ÿ˜Ž

SF Symbols ์ง€์—ญํ™” ์˜ˆ์ œ
class NavigationViewController: UIViewController {
    
    private func setupNavigationButtons() {
        // ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ - RTL ์–ธ์–ด์—์„œ ์ž๋™์œผ๋กœ ๋ฐฉํ–ฅ์ด ๋ฐ”๋€œ
        let backButton = UIButton()
        let backImage = UIImage(systemName: "chevron.left")?
            .withConfiguration(UIImage.SymbolConfiguration(pointSize: 20))
        backButton.setImage(backImage, for: .normal)
        
        // ์•ž์œผ๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ
        let forwardButton = UIButton()
        let forwardImage = UIImage(systemName: "chevron.right")?
            .withConfiguration(UIImage.SymbolConfiguration(pointSize: 20))
        forwardButton.setImage(forwardImage, for: .normal)
        
        // RTL ์–ธ์–ด์—์„œ๋Š” ์ž๋™์œผ๋กœ ์ขŒ์šฐ๊ฐ€ ๋ฐ”๋€œ!
        // ์•„๋ž์–ด: chevron.left๊ฐ€ ์˜ค๋ฅธ์ชฝ์„ ๊ฐ€๋ฆฌํ‚ด
        // ์˜์–ด/ํ•œ๊ตญ์–ด: chevron.left๊ฐ€ ์™ผ์ชฝ์„ ๊ฐ€๋ฆฌํ‚ด
    }
    
    // ํŠน์ • ์‹ฌ๋ณผ์˜ ๋ฐฉํ–ฅ์„ ๊ฐ•์ œ๋กœ ์ง€์ •ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด
    private func createDirectionalSymbol() {
        let config = UIImage.SymbolConfiguration(pointSize: 20)
        
        // ํ•ญ์ƒ ์™ผ์ชฝ์„ ๊ฐ€๋ฆฌํ‚ค๋Š” ํ™”์‚ดํ‘œ (RTL์—์„œ๋„ ๋ณ€ํ•˜์ง€ ์•Š์Œ)
        let leftArrow = UIImage(systemName: "arrow.left", 
                               withConfiguration: config)?
            .withHorizontallyFlippedOrientation()
        
        // ์–ธ์–ด์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ๋ฐฉํ–ฅ์ด ๋ฐ”๋€Œ๋Š” ํ™”์‚ดํ‘œ
        let autoArrow = UIImage(systemName: "arrow.forward", 
                               withConfiguration: config)
    }
}

๐Ÿ”„ RTL(Right-to-Left) ์–ธ์–ด ์ง€์›

์•„๋ž์–ด, ํžˆ๋ธŒ๋ฆฌ์–ด, ํŽ˜๋ฅด์‹œ์•„์–ด ๊ฐ™์€ ์–ธ์–ด๋“ค์€ ์˜ค๋ฅธ์ชฝ์—์„œ ์™ผ์ชฝ์œผ๋กœ ๊ธ€์„ ์จ. ์ด๋Ÿฐ ์–ธ์–ด๋“ค์„ ์ง€์›ํ•˜๋ ค๋ฉด UI ๋ ˆ์ด์•„์›ƒ๋„ ์™„์ „ํžˆ ๋’ค์ง‘์–ด์•ผ ํ•ด! ๐Ÿ˜ฎ

๋‹คํ–‰ํžˆ iOS๋Š” ์ด๋Ÿฐ RTL ์ง€์›์„ ์ •๋ง ์ž˜ ํ•ด์ค˜. Auto Layout์„ ์ œ๋Œ€๋กœ ์‚ฌ์šฉํ–ˆ๋‹ค๋ฉด ๋Œ€๋ถ€๋ถ„ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋ผ! ๐ŸŽ‰

๐Ÿ“ Auto Layout์œผ๋กœ RTL ๋Œ€์‘ํ•˜๊ธฐ

โœ… RTL ์นœํ™”์ ์ธ ์ œ์•ฝ์กฐ๊ฑด ์‚ฌ์šฉํ•˜๊ธฐ ์ข‹์€ ์˜ˆ:
- leadingAnchor / trailingAnchor ์‚ฌ์šฉ
- UIStackView ํ™œ์šฉ
- semanticContentAttribute ์„ค์ •

๋‚˜์œ ์˜ˆ:
- leftAnchor / rightAnchor ์ง์ ‘ ์‚ฌ์šฉ
- ์ ˆ๋Œ€ ์ขŒํ‘œ ์‚ฌ์šฉ
- ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ฐฉํ–ฅ์„ฑ
RTL ๋Œ€์‘ ๋ ˆ์ด์•„์›ƒ ์˜ˆ์ œ
class MessageCell: UITableViewCell {
    
    private let profileImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.layer.cornerRadius = 25
        imageView.clipsToBounds = true
        return imageView
    }()
    
    private let nameLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 16, weight: .semibold)
        return label
    }()
    
    private let messageLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 14)
        label.numberOfLines = 2
        return label
    }()
    
    private let timeLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 12)
        label.textColor = .gray
        return label
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupLayout()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupLayout() {
        [profileImageView, nameLabel, messageLabel, timeLabel].forEach {
            contentView.addSubview($0)
            $0.translatesAutoresizingMaskIntoConstraints = false
        }
        
        NSLayoutConstraint.activate([
            // โœ… leading/trailing ์‚ฌ์šฉ - RTL์—์„œ ์ž๋™์œผ๋กœ ๋’ค์ง‘ํž˜
            profileImageView.leadingAnchor.constraint(
                equalTo: contentView.leadingAnchor, 
                constant: 15
            ),
            profileImageView.centerYAnchor.constraint(
                equalTo: contentView.centerYAnchor
            ),
            profileImageView.widthAnchor.constraint(equalToConstant: 50),
            profileImageView.heightAnchor.constraint(equalToConstant: 50),
            
            // ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ๋‹ค์Œ์— ์œ„์น˜ (RTL์—์„œ๋Š” ์˜ค๋ฅธ์ชฝ์— ์œ„์น˜)
            nameLabel.leadingAnchor.constraint(
                equalTo: profileImageView.trailingAnchor, 
                constant: 12
            ),
            nameLabel.topAnchor.constraint(
                equalTo: contentView.topAnchor, 
                constant: 12
            ),
            nameLabel.trailingAnchor.constraint(
                equalTo: timeLabel.leadingAnchor, 
                constant: -8
            ),
            
            messageLabel.leadingAnchor.constraint(
                equalTo: nameLabel.leadingAnchor
            ),
            messageLabel.topAnchor.constraint(
                equalTo: nameLabel.bottomAnchor, 
                constant: 4
            ),
            messageLabel.trailingAnchor.constraint(
                equalTo: contentView.trailingAnchor, 
                constant: -15
            ),
            
            // ์‹œ๊ฐ„์€ ํ•ญ์ƒ ๋์ชฝ์— ์œ„์น˜ (RTL์—์„œ๋Š” ์™ผ์ชฝ)
            timeLabel.trailingAnchor.constraint(
                equalTo: contentView.trailingAnchor, 
                constant: -15
            ),
            timeLabel.centerYAnchor.constraint(
                equalTo: nameLabel.centerYAnchor
            )
        ])
    }
}

๐ŸŽจ ํ…์ŠคํŠธ ์ •๋ ฌ ์ฒ˜๋ฆฌํ•˜๊ธฐ

์ž์—ฐ์Šค๋Ÿฌ์šด ํ…์ŠคํŠธ ์ •๋ ฌ
class LocalizedTextView: UIView {
    
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 24, weight: .bold)
        // โœ… natural ์ •๋ ฌ - ์–ธ์–ด์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ์กฐ์ •๋จ
        label.textAlignment = .natural
        return label
    }()
    
    private let descriptionLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        // LTR ์–ธ์–ด: ์™ผ์ชฝ ์ •๋ ฌ
        // RTL ์–ธ์–ด: ์˜ค๋ฅธ์ชฝ ์ •๋ ฌ
        label.textAlignment = .natural
        return label
    }()
    
    private let centeredLabel: UILabel = {
        let label = UILabel()
        // ์ค‘์•™ ์ •๋ ฌ์€ ๋ชจ๋“  ์–ธ์–ด์—์„œ ๋™์ผ
        label.textAlignment = .center
        return label
    }()
    
    // ํŠน์ • ๊ฒฝ์šฐ์— ๊ฐ•์ œ๋กœ ๋ฐฉํ–ฅ ์ง€์ •์ด ํ•„์š”ํ•  ๋•Œ
    private func forceLeftAlignment() {
        let label = UILabel()
        // โŒ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด RTL ์–ธ์–ด์—์„œ๋„ ์™ผ์ชฝ ์ •๋ ฌ๋จ
        label.textAlignment = .left
        
        // โœ… ๋Œ€์‹  ์ด๋ ‡๊ฒŒ ํ•˜๋Š” ๊ฒŒ ์ข‹์Œ
        if UIView.userInterfaceLayoutDirection(
            for: label.semanticContentAttribute
        ) == .rightToLeft {
            label.textAlignment = .right
        } else {
            label.textAlignment = .left
        }
    }
}

๐Ÿ”ง UIStackView๋กœ ๊ฐ„ํŽธํ•˜๊ฒŒ RTL ๋Œ€์‘ํ•˜๊ธฐ

UIStackView๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด RTL ๋Œ€์‘์ด ์ •๋ง ์‰ฌ์›Œ์ ธ! ์ž๋™์œผ๋กœ ์ˆœ์„œ๊ฐ€ ๋ฐ”๋€Œ๊ฑฐ๋“ . ๐Ÿ˜

UIStackView RTL ์˜ˆ์ œ
class ProfileHeaderView: UIView {
    
    private let stackView: UIStackView = {
        let stack = UIStackView()
        stack.axis = .horizontal
        stack.spacing = 12
        stack.alignment = .center
        // โœ… ์ด ํ•œ ์ค„๋กœ RTL ์ž๋™ ๋Œ€์‘!
        stack.semanticContentAttribute = .unspecified
        return stack
    }()
    
    private let profileImageView = UIImageView()
    private let nameLabel = UILabel()
    private let badgeImageView = UIImageView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupView() {
        addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        // LTR: [์ด๋ฏธ์ง€] [์ด๋ฆ„] [๋ฑƒ์ง€]
        // RTL: [๋ฑƒ์ง€] [์ด๋ฆ„] [์ด๋ฏธ์ง€]
        // ์ž๋™์œผ๋กœ ์ˆœ์„œ๊ฐ€ ๋ฐ”๋€œ!
        stackView.addArrangedSubview(profileImageView)
        stackView.addArrangedSubview(nameLabel)
        stackView.addArrangedSubview(badgeImageView)
        
        NSLayoutConstraint.activate([
            stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15),
            stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -15),
            stackView.topAnchor.constraint(equalTo: topAnchor, constant: 10),
            stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10)
        ])
    }
}
๐Ÿ’š RTL ํ…Œ์ŠคํŠธ ๊ฟ€ํŒ 1. Xcode Scheme์—์„œ "Application Language"๋ฅผ Arabic๋กœ ์„ค์ •ํ•ด์„œ ํ…Œ์ŠคํŠธ
2. ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์„ค์ •์—์„œ ์–ธ์–ด๋ฅผ ๋ฐ”๊ฟ”๊ฐ€๋ฉฐ ํ™•์ธ
3. Debug View Hierarchy๋กœ ๋ ˆ์ด์•„์›ƒ ํ™•์ธ
4. ์‹ค์ œ RTL ์–ธ์–ด ์‚ฌ์šฉ์ž์—๊ฒŒ ํ”ผ๋“œ๋ฐฑ ๋ฐ›๊ธฐ (๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด!)

๐Ÿ›  ์‹ค์ „ ํŒ๊ณผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์ด์ œ ์‹ค์ „์—์„œ ์ •๋ง ์œ ์šฉํ•œ ํŒ๋“ค์„ ๊ณต์œ ํ•ด๋ณผ๊ฒŒ! ์ด๊ฑด ๋‚ด๊ฐ€ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ๊ฒช์œผ๋ฉด์„œ ๋ฐฐ์šด ๊ฒƒ๋“ค์ด์•ผ. ๐Ÿ˜Š

๐Ÿ“ ๋ฌธ์ž์—ด ํ‚ค ๊ด€๋ฆฌ ์ „๋žต

ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๋ฉด ๋ฌธ์ž์—ด ํ‚ค๊ฐ€ ์ˆ˜๋ฐฑ ๊ฐœ๊ฐ€ ๋  ์ˆ˜ ์žˆ์–ด. ์ด๊ฑธ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒŒ ์ •๋ง ์ค‘์š”ํ•ด! ๐Ÿ—‚

LocalizedString ๋ž˜ํผ ๋งŒ๋“ค๊ธฐ
// Localizable.swift
enum Localizable {
    
    // MARK: - Common
    enum Common {
        static let ok = "common_ok".localized
        static let cancel = "common_cancel".localized
        static let confirm = "common_confirm".localized
        static let delete = "common_delete".localized
        static let save = "common_save".localized
        static let edit = "common_edit".localized
    }
    
    // MARK: - Authentication
    enum Auth {
        static let loginTitle = "auth_login_title".localized
        static let loginButton = "auth_login_button".localized
        static let signupButton = "auth_signup_button".localized
        static let forgotPassword = "auth_forgot_password".localized
        
        static func welcomeMessage(name: String) -> String {
            String(format: "auth_welcome_message".localized, name)
        }
    }
    
    // MARK: - Profile
    enum Profile {
        static let title = "profile_title".localized
        static let editButton = "profile_edit_button".localized
        static let logoutButton = "profile_logout_button".localized
        
        static func itemsCount(_ count: Int) -> String {
            String.localizedStringWithFormat(
                "profile_items_count".localized, 
                count
            )
        }
    }
    
    // MARK: - Errors
    enum Error {
        static let networkError = "error_network".localized
        static let unknownError = "error_unknown".localized
        static let invalidInput = "error_invalid_input".localized
        
        static func customError(message: String) -> String {
            String(format: "error_custom".localized, message)
        }
    }
}

// String Extension
extension String {
    var localized: String {
        return NSLocalizedString(self, comment: "")
    }
    
    func localized(with arguments: CVarArg...) -> String {
        return String(format: self.localized, arguments: arguments)
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
class LoginViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // โœ… ์ด๋ ‡๊ฒŒ ๊น”๋”ํ•˜๊ฒŒ ์‚ฌ์šฉ!
        titleLabel.text = Localizable.Auth.loginTitle
        loginButton.setTitle(Localizable.Auth.loginButton, for: .normal)
        
        // ๋™์  ๋ฉ”์‹œ์ง€
        let userName = "๊น€์ฒ ์ˆ˜"
        welcomeLabel.text = Localizable.Auth.welcomeMessage(name: userName)
    }
    
    private func showError() {
        let alert = UIAlertController(
            title: Localizable.Error.networkError,
            message: Localizable.Error.customError(message: "์—ฐ๊ฒฐ ์‹คํŒจ"),
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(
            title: Localizable.Common.ok, 
            style: .default
        ))
        present(alert, animated: true)
    }
}

์ด๋ ‡๊ฒŒ enum์œผ๋กœ ๊ตฌ์กฐํ™”ํ•˜๋ฉด ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ์žฅ์ ์ด ์žˆ์–ด: ๐ŸŽฏ

1. ์ž๋™์™„์„ฑ: Xcode๊ฐ€ ์ž๋™์œผ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฌธ์ž์—ด์„ ๋ณด์—ฌ์ค˜
2. ํƒ€์ž… ์•ˆ์ •์„ฑ: ์˜คํƒ€๋กœ ์ธํ•œ ๋ฒ„๊ทธ๋ฅผ ์ปดํŒŒ์ผ ํƒ€์ž„์— ์žก์„ ์ˆ˜ ์žˆ์–ด
3. ๊ฐ€๋…์„ฑ: ์ฝ”๋“œ๋งŒ ๋ด๋„ ์–ด๋–ค ๋ฌธ์ž์—ด์ธ์ง€ ๋ช…ํ™•ํ•ด
4. ์œ ์ง€๋ณด์ˆ˜: ๋ฌธ์ž์—ด ํ‚ค๋ฅผ ๋ฐ”๊ฟ”๋„ ํ•œ ๊ณณ๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ๋ผ

๐Ÿ” ๋ฒˆ์—ญ ๋ˆ„๋ฝ ์ฐพ๊ธฐ

ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๋ฉด ๋ฒˆ์—ญ์„ ๊นœ๋นกํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ƒ๊ฒจ. ์ด๊ฑธ ์ž๋™์œผ๋กœ ์ฒดํฌํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์–ด! ๐Ÿ•ต๏ธ

๋ฒˆ์—ญ ๋ˆ„๋ฝ ์ฒดํฌ ์Šคํฌ๋ฆฝํŠธ
#!/bin/bash

# check_localization.sh
# ๋ชจ๋“  .strings ํŒŒ์ผ์˜ ํ‚ค๋ฅผ ๋น„๊ตํ•ด์„œ ๋ˆ„๋ฝ๋œ ๋ฒˆ์—ญ์„ ์ฐพ์Œ

BASE_FILE="en.lproj/Localizable.strings"
LANG_FILES=("ko.lproj/Localizable.strings" "ja.lproj/Localizable.strings")

echo "๐Ÿ” ๋ฒˆ์—ญ ๋ˆ„๋ฝ ์ฒดํฌ ์‹œ์ž‘..."

# Base ํŒŒ์ผ์˜ ๋ชจ๋“  ํ‚ค ์ถ”์ถœ
base_keys=$(grep -o '"[^"]*"' "$BASE_FILE" | grep -o '[^"]*' | sort | uniq)

for lang_file in "${LANG_FILES[@]}"; do
    echo "\n๐Ÿ“ ์ฒดํฌ ์ค‘: $lang_file"
    
    # ํ•ด๋‹น ์–ธ์–ด ํŒŒ์ผ์˜ ํ‚ค ์ถ”์ถœ
    lang_keys=$(grep -o '"[^"]*"' "$lang_file" | grep -o '[^"]*' | sort | uniq)
    
    # ๋ˆ„๋ฝ๋œ ํ‚ค ์ฐพ๊ธฐ
    missing_keys=$(comm -23 <(echo "$base_keys") <(echo "$lang_keys"))
    
    if [ -z "$missing_keys" ]; then
        echo "โœ… ๋ชจ๋“  ๋ฒˆ์—ญ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!"
    else
        echo "โŒ ๋ˆ„๋ฝ๋œ ๋ฒˆ์—ญ:"
        echo "$missing_keys"
    fi
done

echo "\nโœจ ์ฒดํฌ ์™„๋ฃŒ!"

์ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ Xcode์˜ Build Phase์— ์ถ”๊ฐ€ํ•˜๋ฉด, ๋นŒ๋“œํ•  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์ฒดํฌํ•  ์ˆ˜ ์žˆ์–ด! ์ •๋ง ์œ ์šฉํ•˜์ง€? ๐Ÿ˜Ž

๐ŸŽฏ ๋ฒˆ์—ญ ํ’ˆ์งˆ ๊ด€๋ฆฌ

๐ŸŒŸ ์ข‹์€ ๋ฒˆ์—ญ์„ ์œ„ํ•œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… ๋ฌธ๋งฅ์„ ๊ณ ๋ คํ•œ ๋ฒˆ์—ญ (comment ํ™œ์šฉ)
  • โœ… ์ผ๊ด€๋œ ์šฉ์–ด ์‚ฌ์šฉ (์šฉ์–ด์ง‘ ๋งŒ๋“ค๊ธฐ)
  • โœ… ์ž์—ฐ์Šค๋Ÿฌ์šด ํ‘œํ˜„ (์ง์—ญ X)
  • โœ… ๋ฌธํ™”์  ์ฐจ์ด ๊ณ ๋ ค
  • โœ… ๊ธธ์ด ์ œํ•œ ํ™•์ธ (UI์— ๋งž๊ฒŒ)
  • โœ… ์ „๋ฌธ ๋ฒˆ์—ญ๊ฐ€ ๊ฒ€ํ†  (๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด)
  • โœ… ๋„ค์ดํ‹ฐ๋ธŒ ์Šคํ”ผ์ปค ํ…Œ์ŠคํŠธ
๋ฒˆ์—ญ ๊ฐ€์ด๋“œ๋ผ์ธ ๋ฌธ์„œ ์˜ˆ์‹œ
// TranslationGuidelines.md

# ๋ฒˆ์—ญ ๊ฐ€์ด๋“œ๋ผ์ธ

## ์šฉ์–ด ํ†ต์ผ
- User โ†’ ์‚ฌ์šฉ์ž (์œ ์ € X)
- Settings โ†’ ์„ค์ •
- Profile โ†’ ํ”„๋กœํ•„
- Message โ†’ ๋ฉ”์‹œ์ง€

## ํ†ค ์•ค ๋งค๋„ˆ
- ์กด๋Œ“๋ง ์‚ฌ์šฉ (~ํ•ฉ๋‹ˆ๋‹ค, ~ํ•˜์„ธ์š”)
- ์นœ๊ทผํ•˜๊ณ  ๋”ฐ๋œปํ•œ ๋А๋‚Œ
- ๊ธฐ์ˆ  ์šฉ์–ด๋Š” ํ•œ๊ธ€ํ™”๋œ ํ‘œํ˜„ ์šฐ์„ 

## ์ฃผ์˜์‚ฌํ•ญ
- ๋ฒ„ํŠผ ํ…์ŠคํŠธ๋Š” 2-3๋‹จ์–ด ์ด๋‚ด
- ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋Š” ๊ตฌ์ฒด์ ์œผ๋กœ
- ๊ธ์ •์ ์ธ ํ‘œํ˜„ ์‚ฌ์šฉ

## ๋ฌธํ™”์  ๊ณ ๋ ค์‚ฌํ•ญ
- ์ƒ‰์ƒ ์˜๋ฏธ (๋นจ๊ฐ• = ์œ„ํ—˜/ํ–‰์šด)
- ์ˆซ์ž ์˜๋ฏธ (4 = ๋ถˆ๊ธธ)
- ์ œ์Šค์ฒ˜ ์ฐจ์ด

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™”

NSLocalizedString์€ ๋งค๋ฒˆ ํŒŒ์ผ์„ ์ฝ์–ด์˜ค๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์บ์‹ฑ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ ๊ฑฑ์ •์€ ์•ˆ ํ•ด๋„ ๋ผ! ํ•˜์ง€๋งŒ ๋” ์ตœ์ ํ™”ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด? ๐Ÿค”

๋ฌธ์ž์—ด ์บ์‹ฑ ์ „๋žต
class LocalizationManager {
    static let shared = LocalizationManager()
    
    private var cache: [String: String] = [:]
    private let queue = DispatchQueue(label: "com.app.localization", 
                                     attributes: .concurrent)
    
    private init() {
        // ์•ฑ ์‹œ์ž‘ ์‹œ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ๋ฌธ์ž์—ด ๋ฏธ๋ฆฌ ๋กœ๋“œ
        preloadCommonStrings()
    }
    
    private func preloadCommonStrings() {
        let commonKeys = [
            "common_ok",
            "common_cancel",
            "common_confirm",
            "common_delete",
            "common_save"
        ]
        
        commonKeys.forEach { key in
            cache[key] = NSLocalizedString(key, comment: "")
        }
    }
    
    func localizedString(for key: String, comment: String = "") -> String {
        // ์บ์‹œ ํ™•์ธ
        if let cached = queue.sync(execute: { cache[key] }) {
            return cached
        }
        
        // ์บ์‹œ์— ์—†์œผ๋ฉด ๋กœ๋“œํ•˜๊ณ  ์ €์žฅ
        let localized = NSLocalizedString(key, comment: comment)
        queue.async(flags: .barrier) { [weak self] in
            self?.cache[key] = localized
        }
        
        return localized
    }
    
    func clearCache() {
        queue.async(flags: .barrier) { [weak self] in
            self?.cache.removeAll()
        }
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
extension String {
    var localizedCached: String {
        return LocalizationManager.shared.localizedString(for: self)
    }
}

ํ•˜์ง€๋งŒ ์†”์งํžˆ ๋งํ•˜๋ฉด, ๋Œ€๋ถ€๋ถ„์˜ ์•ฑ์—์„œ๋Š” ์ด๋Ÿฐ ์ตœ์ ํ™”๊ฐ€ ํ•„์š” ์—†์–ด. iOS๊ฐ€ ์ด๋ฏธ ์ถฉ๋ถ„ํžˆ ๋น ๋ฅด๊ฑฐ๋“ ! ๐Ÿ˜Š
์ •๋ง ์ˆ˜์ฒœ ๊ฐœ์˜ ๋ฌธ์ž์—ด์„ ๋™์‹œ์— ๋กœ๋“œํ•˜๋Š” ํŠน์ˆ˜ํ•œ ๊ฒฝ์šฐ์—๋งŒ ๊ณ ๋ คํ•ด๋ด.

๊ตญ์ œํ™” ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋ชจ๋“  ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด์„ NSLocalizedString์œผ๋กœ ๋ณ€๊ฒฝ Localizable.strings ํŒŒ์ผ์— ๋ชจ๋“  ์–ธ์–ด ๋ฒˆ์—ญ ์ถ”๊ฐ€ ๋ณต์ˆ˜ํ˜• ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ stringsdict ํŒŒ์ผ ์ƒ์„ฑ ์ด๋ฏธ์ง€์™€ ์—์…‹๋„ ์ง€์—ญํ™” ์„ค์ • Auto Layout์œผ๋กœ RTL ์–ธ์–ด ๋Œ€์‘ ๊ฐ ์–ธ์–ด๋ณ„๋กœ ์‹ค์ œ ๊ธฐ๊ธฐ์—์„œ ํ…Œ์ŠคํŠธ โœจ

๐Ÿ› ์ž์ฃผ ๋งŒ๋‚˜๋Š” ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ๋ฒ•

๊ตญ์ œํ™” ์ž‘์—…์„ ํ•˜๋‹ค ๋ณด๋ฉด ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ œ๋“ค์„ ๋งŒ๋‚˜๊ฒŒ ๋ผ. ๋‚ด๊ฐ€ ๊ฒช์—ˆ๋˜ ๋Œ€ํ‘œ์ ์ธ ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ๋ฒ•์„ ๊ณต์œ ํ• ๊ฒŒ! ๐Ÿ”ง

โŒ ๋ฌธ์ œ 1: ๋ฒˆ์—ญ์ด ์ ์šฉ๋˜์ง€ ์•Š์•„์š”!

์ฆ์ƒ NSLocalizedString์„ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ ์˜์–ด(๋˜๋Š” Base ์–ธ์–ด)๋งŒ ๊ณ„์† ๋‚˜์™€์š”. ๐Ÿ˜ฑ
ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
// 1. ํŒŒ์ผ ์ด๋ฆ„ ํ™•์ธ
// โŒ ์ž˜๋ชป๋œ ์ด๋ฆ„
"Localized.strings"
"localizable.strings"
"LocalizableStrings.strings"

// โœ… ์˜ฌ๋ฐ”๋ฅธ ์ด๋ฆ„
"Localizable.strings"

// 2. ํŒŒ์ผ ์œ„์น˜ ํ™•์ธ
// Localizable.strings ํŒŒ์ผ์ด ์˜ฌ๋ฐ”๋ฅธ .lproj ํด๋” ์•ˆ์— ์žˆ๋Š”์ง€ ํ™•์ธ
// ์˜ˆ: ko.lproj/Localizable.strings

// 3. ๋นŒ๋“œ ํƒ€๊ฒŸ ํ™•์ธ
// ํŒŒ์ผ ์ธ์ŠคํŽ™ํ„ฐ์—์„œ Target Membership์ด ์ฒดํฌ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ

// 4. ํด๋ฆฐ ๋นŒ๋“œ
// Product โ†’ Clean Build Folder (Cmd + Shift + K)
// ๊ทธ ๋‹ค์Œ ๋‹ค์‹œ ๋นŒ๋“œ

// 5. ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์–ธ์–ด ์„ค์ • ํ™•์ธ
// Settings โ†’ General โ†’ Language & Region
// ์›ํ•˜๋Š” ์–ธ์–ด๊ฐ€ ์„ ํƒ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ

// 6. ๋””๋ฒ„๊น… ์ฝ”๋“œ๋กœ ํ™•์ธ
let currentLanguage = Locale.preferredLanguages.first
print("Current language: \(currentLanguage ?? "unknown")")

let testString = NSLocalizedString("test_key", comment: "")
print("Localized string: \(testString)")

โŒ ๋ฌธ์ œ 2: ํŠน์ˆ˜๋ฌธ์ž๊ฐ€ ๊นจ์ ธ์š”!

์ฆ์ƒ ํ•œ๊ธ€์ด๋‚˜ ์ผ๋ณธ์–ด, ํŠน์ˆ˜๋ฌธ์ž๊ฐ€ ์ด์ƒํ•˜๊ฒŒ ํ‘œ์‹œ๋ผ์š”. ๐Ÿคฏ
ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
// 1. ํŒŒ์ผ ์ธ์ฝ”๋”ฉ ํ™•์ธ
// Xcode์—์„œ Localizable.strings ํŒŒ์ผ ์„ ํƒ
// File Inspector โ†’ Text Settings โ†’ Text Encoding
// "UTF-8" ์„ ํƒ

// 2. ์ด๋ฏธ ๊นจ์ง„ ํŒŒ์ผ ๋ณต๊ตฌ
// ํ„ฐ๋ฏธ๋„์—์„œ ํŒŒ์ผ ์ธ์ฝ”๋”ฉ ๋ณ€ํ™˜
// iconv -f ISO-8859-1 -t UTF-8 Localizable.strings > Localizable_fixed.strings

// 3. ์ƒˆ๋กœ ์ž‘์„ฑํ•  ๋•Œ ์ฃผ์˜
// Xcode์—์„œ ์ง์ ‘ ์ž‘์„ฑํ•˜๋ฉด ์ž๋™์œผ๋กœ UTF-8๋กœ ์ €์žฅ๋จ
// ์™ธ๋ถ€ ์—๋””ํ„ฐ ์‚ฌ์šฉ ์‹œ ๋ฐ˜๋“œ์‹œ UTF-8๋กœ ์ €์žฅ

// 4. ์ด์Šค์ผ€์ดํ”„ ์‹œํ€€์Šค ์‚ฌ์šฉ
"special_chars" = "๋”ฐ์˜ดํ‘œ: \" ๋ฐฑ์Šฌ๋ž˜์‹œ: \\ ์ค„๋ฐ”๊ฟˆ: \n";

โŒ ๋ฌธ์ œ 3: ๋ฌธ์ž์—ด์ด UI์— ๋‹ค ์•ˆ ๋“ค์–ด๊ฐ€์š”!

์ฆ์ƒ ์˜์–ด๋Š” ๊ดœ์ฐฎ์€๋ฐ ๋…์ผ์–ด๋‚˜ ํ•œ๊ตญ์–ด๋กœ ๋ฐ”๊พธ๋ฉด ํ…์ŠคํŠธ๊ฐ€ ์ž˜๋ ค์š”. โœ‚๏ธ
ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
class AdaptiveButton: UIButton {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupButton()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupButton()
    }
    
    private func setupButton() {
        // 1. ์ž๋™ ํฐํŠธ ํฌ๊ธฐ ์กฐ์ •
        titleLabel?.adjustsFontSizeToFitWidth = true
        titleLabel?.minimumScaleFactor = 0.8
        
        // 2. ์—ฌ๋Ÿฌ ์ค„ ํ—ˆ์šฉ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ)
        titleLabel?.numberOfLines = 0
        titleLabel?.lineBreakMode = .byWordWrapping
        
        // 3. ์ถฉ๋ถ„ํ•œ ํŒจ๋”ฉ ์„ค์ •
        contentEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
    }
}

class AdaptiveLabel: UILabel {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupLabel()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupLabel()
    }
    
    private func setupLabel() {
        // 1. ์ž๋™ ์ค„๋ฐ”๊ฟˆ
        numberOfLines = 0
        lineBreakMode = .byWordWrapping
        
        // 2. ๋™์  ํƒ€์ž… ์ง€์›
        adjustsFontForContentSizeCategory = true
        
        // 3. ์ตœ์†Œ ํฐํŠธ ํฌ๊ธฐ ์„ค์ •
        adjustsFontSizeToFitWidth = true
        minimumScaleFactor = 0.7
    }
}

// 3. ์ œ์•ฝ์กฐ๊ฑด ์šฐ์„ ์ˆœ์œ„ ์กฐ์ •
class FlexibleView: UIView {
    
    private let label = UILabel()
    
    private func setupConstraints() {
        label.translatesAutoresizingMaskIntoConstraints = false
        addSubview(label)
        
        let widthConstraint = label.widthAnchor.constraint(equalToConstant: 200)
        // ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋‚ฎ์ถฐ์„œ ํ•„์š”ํ•˜๋ฉด ๋Š˜์–ด๋‚  ์ˆ˜ ์žˆ๊ฒŒ
        widthConstraint.priority = .defaultHigh
        
        NSLayoutConstraint.activate([
            label.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 15),
            label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -15),
            label.centerYAnchor.constraint(equalTo: centerYAnchor),
            widthConstraint
        ])
    }
}

โŒ ๋ฌธ์ œ 4: ๋‚ ์งœ/์ˆซ์ž ํ˜•์‹์ด ์ด์ƒํ•ด์š”!

์ฆ์ƒ ๋‚ ์งœ๊ฐ€ "12/31/2023"์œผ๋กœ ๋‚˜์™€์•ผ ํ•˜๋Š”๋ฐ "2023-12-31"๋กœ ๋‚˜์™€์š”. ๐Ÿ“…
ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
class LocalizedFormatters {
    
    // MARK: - Date Formatter
    static func formatDate(_ date: Date, style: DateFormatter.Style = .medium) -> String {
        let formatter = DateFormatter()
        formatter.dateStyle = style
        formatter.timeStyle = .none
        // ์‚ฌ์šฉ์ž์˜ ๋กœ์ผ€์ผ์— ๋งž๊ฒŒ ์ž๋™ ์กฐ์ •
        formatter.locale = Locale.current
        return formatter.string(from: date)
    }
    
    static func formatDateTime(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .short
        formatter.locale = Locale.current
        return formatter.string(from: date)
    }
    
    // MARK: - Number Formatter
    static func formatNumber(_ number: Double, decimals: Int = 2) -> String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.minimumFractionDigits = decimals
        formatter.maximumFractionDigits = decimals
        formatter.locale = Locale.current
        return formatter.string(from: NSNumber(value: number)) ?? "\(number)"
    }
    
    // MARK: - Currency Formatter
    static func formatCurrency(_ amount: Double, currencyCode: String = "USD") -> String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.currencyCode = currencyCode
        formatter.locale = Locale.current
        return formatter.string(from: NSNumber(value: amount)) ?? "\(amount)"
    }
    
    // MARK: - Percentage Formatter
    static func formatPercentage(_ value: Double) -> String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .percent
        formatter.minimumFractionDigits = 0
        formatter.maximumFractionDigits = 2
        formatter.locale = Locale.current
        return formatter.string(from: NSNumber(value: value)) ?? "\(value * 100)%"
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
class StatisticsViewController: UIViewController {
    
    private func updateUI() {
        let now = Date()
        let price = 1234.56
        let percentage = 0.1234
        
        // ๋‚ ์งœ: ํ•œ๊ตญ "2024๋…„ 1์›” 15์ผ", ๋ฏธ๊ตญ "January 15, 2024"
        dateLabel.text = LocalizedFormatters.formatDate(now)
        
        // ์ˆซ์ž: ํ•œ๊ตญ "1,234.56", ๋…์ผ "1.234,56"
        numberLabel.text = LocalizedFormatters.formatNumber(price)
        
        // ํ†ตํ™”: ํ•œ๊ตญ "โ‚ฉ1,235", ๋ฏธ๊ตญ "$1,234.56"
        priceLabel.text = LocalizedFormatters.formatCurrency(price, currencyCode: "KRW")
        
        // ํผ์„ผํŠธ: "12.34%"
        percentLabel.text = LocalizedFormatters.formatPercentage(percentage)
    }
}

โŒ ๋ฌธ์ œ 5: ์•ฑ ๋‚ด์—์„œ ์–ธ์–ด๋ฅผ ๋ฐ”๊พธ๊ณ  ์‹ถ์–ด์š”!

์ฆ์ƒ ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ ๋‚ด์—์„œ ์ง์ ‘ ์–ธ์–ด๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ณ  ์‹ถ์–ด์š”. ๐ŸŒ
ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
class LanguageManager {
    static let shared = LanguageManager()
    
    private let languageKey = "AppLanguage"
    
    var currentLanguage: String {
        get {
            return UserDefaults.standard.string(forKey: languageKey) 
                ?? Locale.preferredLanguages.first 
                ?? "en"
        }
        set {
            UserDefaults.standard.set(newValue, forKey: languageKey)
            UserDefaults.standard.synchronize()
        }
    }
    
    func setLanguage(_ languageCode: String) {
        currentLanguage = languageCode
        
        // ์•ฑ ์žฌ์‹œ์ž‘ ํ•„์š” ์•Œ๋ฆผ
        NotificationCenter.default.post(
            name: NSNotification.Name("LanguageChanged"), 
            object: nil
        )
    }
    
    func localizedString(for key: String, comment: String = "") -> String {
        guard let path = Bundle.main.path(forResource: currentLanguage, ofType: "lproj"),
              let bundle = Bundle(path: path) else {
            return NSLocalizedString(key, comment: comment)
        }
        
        return bundle.localizedString(forKey: key, value: nil, table: nil)
    }
}

// ์–ธ์–ด ์„ ํƒ ํ™”๋ฉด
class LanguageSettingsViewController: UIViewController {
    
    private let languages = [
        ("en", "English", "๐Ÿ‡บ๐Ÿ‡ธ"),
        ("ko", "ํ•œ๊ตญ์–ด", "๐Ÿ‡ฐ๐Ÿ‡ท"),
        ("ja", "ๆ—ฅๆœฌ่ชž", "๐Ÿ‡ฏ๐Ÿ‡ต"),
        ("zh-Hans", "็ฎ€ไฝ“ไธญๆ–‡", "๐Ÿ‡จ๐Ÿ‡ณ"),
        ("es", "Espaรฑol", "๐Ÿ‡ช๐Ÿ‡ธ")
    ]
    
    private let tableView = UITableView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
        
        // ์–ธ์–ด ๋ณ€๊ฒฝ ๊ฐ์ง€
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(languageChanged),
            name: NSNotification.Name("LanguageChanged"),
            object: nil
        )
    }
    
    private func setupTableView() {
        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "LanguageCell")
        
        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    @objc private func languageChanged() {
        // ์•ฑ ์žฌ์‹œ์ž‘ ์•ˆ๋‚ด
        let alert = UIAlertController(
            title: LanguageManager.shared.localizedString(for: "language_changed_title"),
            message: LanguageManager.shared.localizedString(for: "language_changed_message"),
            preferredStyle: .alert
        )
        
        alert.addAction(UIAlertAction(
            title: LanguageManager.shared.localizedString(for: "restart_now"),
            style: .default
        ) { _ in
            // ์•ฑ ์žฌ์‹œ์ž‘ (์‹ค์ œ๋กœ๋Š” ๋ฃจํŠธ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ๊ต์ฒด)
            self.restartApp()
        })
        
        present(alert, animated: true)
    }
    
    private func restartApp() {
        guard let window = UIApplication.shared.windows.first else { return }
        
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let rootVC = storyboard.instantiateInitialViewController()
        
        window.rootViewController = rootVC
        
        UIView.transition(with: window,
                         duration: 0.3,
                         options: .transitionCrossDissolve,
                         animations: nil,
                         completion: nil)
    }
}

extension LanguageSettingsViewController: UITableViewDelegate, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return languages.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "LanguageCell", for: indexPath)
        let language = languages[indexPath.row]
        
        cell.textLabel?.text = "\(language.2) \(language.1)"
        
        // ํ˜„์žฌ ์„ ํƒ๋œ ์–ธ์–ด ํ‘œ์‹œ
        if LanguageManager.shared.currentLanguage.hasPrefix(language.0) {
            cell.accessoryType = .checkmark
        } else {
            cell.accessoryType = .none
        }
        
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        
        let language = languages[indexPath.row]
        LanguageManager.shared.setLanguage(language.0)
        
        tableView.reloadData()
    }
}
๐Ÿ’š ์–ธ์–ด ์ „ํ™˜ ํŒ 1. ๊ฐ€๋Šฅํ•˜๋ฉด ์‹œ์Šคํ…œ ์–ธ์–ด ์„ค์ •์„ ๋”ฐ๋ฅด๋Š” ๊ฒŒ ์ข‹์•„ (์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ธก๋ฉด์—์„œ)
2. ์•ฑ ๋‚ด ์–ธ์–ด ์ „ํ™˜์„ ์ œ๊ณตํ•œ๋‹ค๋ฉด, ์„ค์ •์„ ๋ช…ํ™•ํ•œ ๊ณณ์— ๋ฐฐ์น˜ํ•ด
3. ์–ธ์–ด ๋ณ€๊ฒฝ ํ›„ ์•ฑ ์žฌ์‹œ์ž‘์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ช…ํ™•ํžˆ ์•ˆ๋‚ดํ•ด
4. ์ผ๋ถ€ ํ™”๋ฉด๋งŒ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ์ „์ฒด ์•ฑ์„ ์žฌ์‹œ์ž‘ํ•˜๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด

๐ŸŽ“ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๋‹ค๊ตญ์–ด ์‡ผํ•‘๋ชฐ ์•ฑ

์ž, ์ด์ œ ๋ฐฐ์šด ๊ฑธ ๋ชจ๋‘ ํ™œ์šฉํ•ด์„œ ์‹ค์ „ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ๊ฐ„๋‹จํ•œ ์‡ผํ•‘๋ชฐ ์•ฑ์˜ ์ƒํ’ˆ ์ƒ์„ธ ํ™”๋ฉด์„ ๋‹ค๊ตญ์–ด๋กœ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ. ๐Ÿ›

์ด ์˜ˆ์ œ๋Š” ์žฌ๋Šฅ๋„ท์—์„œ ํ”„๋กœ์ ํŠธ๋ฅผ ๋ฐ›์•˜์„ ๋•Œ ์‹ค์ œ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์˜ ์ฝ”๋“œ์•ผ! ๐Ÿ’ผ

๐Ÿ“ฑ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

Product ๋ชจ๋ธ
struct Product {
    let id: String
    let nameKey: String  // Localizable.strings์˜ ํ‚ค
    let descriptionKey: String
    let price: Double
    let currency: String
    let imageURL: String
    let stock: Int
    let rating: Double
    let reviewCount: Int
}

// ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ
extension Product {
    static let sampleProducts = [
        Product(
            id: "1",
            nameKey: "product_smartphone_name",
            descriptionKey: "product_smartphone_desc",
            price: 999.99,
            currency: "USD",
            imageURL: "smartphone.jpg",
            stock: 15,
            rating: 4.5,
            reviewCount: 1234
        ),
        Product(
            id: "2",
            nameKey: "product_laptop_name",
            descriptionKey: "product_laptop_desc",
            price: 1499.99,
            currency: "USD",
            imageURL: "laptop.jpg",
            stock: 8,
            rating: 4.8,
            reviewCount: 856
        )
    ]
}
ProductDetailViewController
class ProductDetailViewController: UIViewController {
    
    // MARK: - Properties
    private var product: Product
    
    // MARK: - UI Components
    private let scrollView: UIScrollView = {
        let scroll = UIScrollView()
        scroll.showsVerticalScrollIndicator = true
        return scroll
    }()
    
    private let contentView = UIView()
    
    private let productImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        imageView.backgroundColor = .systemGray6
        imageView.layer.cornerRadius = 12
        imageView.clipsToBounds = true
        return imageView
    }()
    
    private let nameLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 24, weight: .bold)
        label.numberOfLines = 0
        return label
    }()
    
    private let priceLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 28, weight: .heavy)
        label.textColor = .systemRed
        return label
    }()
    
    private let ratingStackView: UIStackView = {
        let stack = UIStackView()
        stack.axis = .horizontal
        stack.spacing = 8
        stack.alignment = .center
        return stack
    }()
    
    private let stockLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 14)
        label.textColor = .systemGray
        return label
    }()
    
    private let descriptionTitleLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 18, weight: .semibold)
        return label
    }()
    
    private let descriptionLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 16)
        label.numberOfLines = 0
        label.textColor = .darkGray
        return label
    }()
    
    private let addToCartButton: UIButton = {
        let button = UIButton(type: .system)
        button.backgroundColor = .systemBlue
        button.setTitleColor(.white, for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
        button.layer.cornerRadius = 12
        return button
    }()
    
    private let buyNowButton: UIButton = {
        let button = UIButton(type: .system)
        button.backgroundColor = .systemGreen
        button.setTitleColor(.white, for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
        button.layer.cornerRadius = 12
        return button
    }()
    
    // MARK: - Initialization
    init(product: Product) {
        self.product = product
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        configureWithProduct()
        setupActions()
    }
    
    // MARK: - Setup
    private func setupUI() {
        view.backgroundColor = .systemBackground
        
        view.addSubview(scrollView)
        scrollView.addSubview(contentView)
        
        [productImageView, nameLabel, priceLabel, ratingStackView,
         stockLabel, descriptionTitleLabel, descriptionLabel,
         addToCartButton, buyNowButton].forEach {
            contentView.addSubview($0)
            $0.translatesAutoresizingMaskIntoConstraints = false
        }
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        contentView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            
            contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
            
            productImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
            productImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            productImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
            productImageView.heightAnchor.constraint(equalToConstant: 300),
            
            nameLabel.topAnchor.constraint(equalTo: productImageView.bottomAnchor, constant: 20),
            nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
            
            priceLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 12),
            priceLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            
            ratingStackView.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 12),
            ratingStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            
            stockLabel.topAnchor.constraint(equalTo: ratingStackView.bottomAnchor, constant: 8),
            stockLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            
            descriptionTitleLabel.topAnchor.constraint(equalTo: stockLabel.bottomAnchor, constant: 24),
            descriptionTitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            descriptionTitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
            
            descriptionLabel.topAnchor.constraint(equalTo: descriptionTitleLabel.bottomAnchor, constant: 12),
            descriptionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            descriptionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
            
            addToCartButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 30),
            addToCartButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            addToCartButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
            addToCartButton.heightAnchor.constraint(equalToConstant: 50),
            
            buyNowButton.topAnchor.constraint(equalTo: addToCartButton.bottomAnchor, constant: 12),
            buyNowButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            buyNowButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
            buyNowButton.heightAnchor.constraint(equalToConstant: 50),
            buyNowButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30)
        ])
    }
    
    private func configureWithProduct() {
        // ์ƒํ’ˆ ์ด๋ฆ„ (์ง€์—ญํ™”)
        nameLabel.text = NSLocalizedString(product.nameKey, comment: "์ƒํ’ˆ ์ด๋ฆ„")
        
        // ๊ฐ€๊ฒฉ (ํ†ตํ™” ํ˜•์‹ ์ง€์—ญํ™”)
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.currencyCode = product.currency
        formatter.locale = Locale.current
        priceLabel.text = formatter.string(from: NSNumber(value: product.price))
        
        // ํ‰์  ๋ฐ ๋ฆฌ๋ทฐ ์ˆ˜
        setupRatingView()
        
        // ์žฌ๊ณ  (๋ณต์ˆ˜ํ˜• ์ฒ˜๋ฆฌ)
        let stockFormat = NSLocalizedString("product_stock_count", comment: "์žฌ๊ณ  ์ˆ˜๋Ÿ‰")
        stockLabel.text = String.localizedStringWithFormat(stockFormat, product.stock)
        
        // ์„ค๋ช… ์ œ๋ชฉ
        descriptionTitleLabel.text = NSLocalizedString("product_description_title", 
                                                       comment: "์ƒํ’ˆ ์„ค๋ช… ์ œ๋ชฉ")
        
        // ์ƒํ’ˆ ์„ค๋ช… (์ง€์—ญํ™”)
        descriptionLabel.text = NSLocalizedString(product.descriptionKey, 
                                                 comment: "์ƒํ’ˆ ์„ค๋ช…")
        
        // ๋ฒ„ํŠผ ํ…์ŠคํŠธ
        addToCartButton.setTitle(NSLocalizedString("product_add_to_cart", 
                                                   comment: "์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋‹ด๊ธฐ ๋ฒ„ํŠผ"), 
                                for: .normal)
        buyNowButton.setTitle(NSLocalizedString("product_buy_now", 
                                               comment: "๋ฐ”๋กœ ๊ตฌ๋งค ๋ฒ„ํŠผ"), 
                             for: .normal)
        
        // ๋„ค๋น„๊ฒŒ์ด์…˜ ํƒ€์ดํ‹€
        navigationItem.title = NSLocalizedString("product_detail_title", 
                                                comment: "์ƒํ’ˆ ์ƒ์„ธ ํ™”๋ฉด ์ œ๋ชฉ")
    }
    
    private func setupRatingView() {
        // ๋ณ„์  ํ‘œ์‹œ
        let starsLabel = UILabel()
        starsLabel.text = String(repeating: "โญ๏ธ", count: Int(product.rating))
        starsLabel.font = .systemFont(ofSize: 16)
        
        // ํ‰์  ์ˆซ์ž
        let ratingLabel = UILabel()
        ratingLabel.text = String(format: "%.1f", product.rating)
        ratingLabel.font = .systemFont(ofSize: 16, weight: .semibold)
        
        // ๋ฆฌ๋ทฐ ์ˆ˜ (๋ณต์ˆ˜ํ˜• ์ฒ˜๋ฆฌ)
        let reviewLabel = UILabel()
        let reviewFormat = NSLocalizedString("product_review_count", comment: "๋ฆฌ๋ทฐ ์ˆ˜")
        reviewLabel.text = "(\(String.localizedStringWithFormat(reviewFormat, product.reviewCount)))"
        reviewLabel.font = .systemFont(ofSize: 14)
        reviewLabel.textColor = .systemGray
        
        ratingStackView.addArrangedSubview(starsLabel)
        ratingStackView.addArrangedSubview(ratingLabel)
        ratingStackView.addArrangedSubview(reviewLabel)
    }
    
    private func setupActions() {
        addToCartButton.addTarget(self, 
                                 action: #selector(addToCartTapped), 
                                 for: .touchUpInside)
        buyNowButton.addTarget(self, 
                              action: #selector(buyNowTapped), 
                              for: .touchUpInside)
    }
    
    // MARK: - Actions
    @objc private func addToCartTapped() {
        let message = NSLocalizedString("product_added_to_cart", 
                                       comment: "์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ถ”๊ฐ€ ์™„๋ฃŒ ๋ฉ”์‹œ์ง€")
        showAlert(message: message)
    }
    
    @objc private func buyNowTapped() {
        let title = NSLocalizedString("purchase_confirm_title", 
                                     comment: "๊ตฌ๋งค ํ™•์ธ ์ œ๋ชฉ")
        let message = String(format: NSLocalizedString("purchase_confirm_message", 
                                                       comment: "๊ตฌ๋งค ํ™•์ธ ๋ฉ”์‹œ์ง€"),
                           priceLabel.text ?? "")
        
        let alert = UIAlertController(title: title, 
                                     message: message, 
                                     preferredStyle: .alert)
        
        alert.addAction(UIAlertAction(
            title: NSLocalizedString("common_confirm", comment: "ํ™•์ธ"),
            style: .default
        ) { [weak self] _ in
            self?.processPurchase()
        })
        
        alert.addAction(UIAlertAction(
            title: NSLocalizedString("common_cancel", comment: "์ทจ์†Œ"),
            style: .cancel
        ))
        
        present(alert, animated: true)
    }
    
    private func processPurchase() {
        let message = NSLocalizedString("purchase_success", 
                                       comment: "๊ตฌ๋งค ์„ฑ๊ณต ๋ฉ”์‹œ์ง€")
        showAlert(message: message)
    }
    
    private func showAlert(message: String) {
        let alert = UIAlertController(title: nil, 
                                     message: message, 
                                     preferredStyle: .alert)
        alert.addAction(UIAlertAction(
            title: NSLocalizedString("common_ok", comment: "ํ™•์ธ"),
            style: .default
        ))
        present(alert, animated: true)
    }
}

๐Ÿ“ ๋ฒˆ์—ญ ํŒŒ์ผ

Localizable.strings (English)
/* Common */
"common_ok" = "OK";
"common_cancel" = "Cancel";
"common_confirm" = "Confirm";

/* Product Detail */
"product_detail_title" = "Product Details";
"product_description_title" = "Description";
"product_add_to_cart" = "Add to Cart";
"product_buy_now" = "Buy Now";
"product_added_to_cart" = "Product added to cart successfully!";

/* Product Info */
"product_smartphone_name" = "Premium Smartphone X";
"product_smartphone_desc" = "Experience the latest technology with our flagship smartphone. Features include a stunning display, powerful processor, and advanced camera system.";
"product_laptop_name" = "Professional Laptop Pro";
"product_laptop_desc" = "Perfect for professionals and creators. High-performance processor, brilliant display, and all-day battery life.";

/* Purchase */
"purchase_confirm_title" = "Confirm Purchase";
"purchase_confirm_message" = "Do you want to purchase this item for %@?";
"purchase_success" = "Purchase completed successfully!";
Localizable.strings (Korean)
/* Common */
"common_ok" = "ํ™•์ธ";
"common_cancel" = "์ทจ์†Œ";
"common_confirm" = "ํ™•์ธ";

/* Product Detail */
"product_detail_title" = "์ƒํ’ˆ ์ƒ์„ธ";
"product_description_title" = "์ƒํ’ˆ ์„ค๋ช…";
"product_add_to_cart" = "์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋‹ด๊ธฐ";
"product_buy_now" = "๋ฐ”๋กœ ๊ตฌ๋งค";
"product_added_to_cart" = "์ƒํ’ˆ์ด ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด๊ฒผ์Šต๋‹ˆ๋‹ค!";

/* Product Info */
"product_smartphone_name" = "ํ”„๋ฆฌ๋ฏธ์—„ ์Šค๋งˆํŠธํฐ X";
"product_smartphone_desc" = "์ตœ์‹  ๊ธฐ์ˆ ์ด ์ง‘์•ฝ๋œ ํ”Œ๋ž˜๊ทธ์‹ญ ์Šค๋งˆํŠธํฐ์„ ๊ฒฝํ—˜ํ•˜์„ธ์š”. ๋†€๋ผ์šด ๋””์Šคํ”Œ๋ ˆ์ด, ๊ฐ•๋ ฅํ•œ ํ”„๋กœ์„ธ์„œ, ๊ทธ๋ฆฌ๊ณ  ์ง„๋ณด๋œ ์นด๋ฉ”๋ผ ์‹œ์Šคํ…œ์„ ๊ฐ–์ถ”๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.";
"product_laptop_name" = "ํ”„๋กœํŽ˜์…”๋„ ๋…ธํŠธ๋ถ Pro";
"product_laptop_desc" = "์ „๋ฌธ๊ฐ€์™€ ํฌ๋ฆฌ์—์ดํ„ฐ๋ฅผ ์œ„ํ•œ ์™„๋ฒฝํ•œ ์„ ํƒ. ๊ณ ์„ฑ๋Šฅ ํ”„๋กœ์„ธ์„œ, ์„ ๋ช…ํ•œ ๋””์Šคํ”Œ๋ ˆ์ด, ๊ทธ๋ฆฌ๊ณ  ํ•˜๋ฃจ ์ข…์ผ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฐฐํ„ฐ๋ฆฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.";

/* Purchase */
"purchase_confirm_title" = "๊ตฌ๋งค ํ™•์ธ";
"purchase_confirm_message" = "%@ ์ƒํ’ˆ์„ ๊ตฌ๋งคํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?";
"purchase_success" = "๊ตฌ๋งค๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!";
Localizable.stringsdict (English)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>product_stock_count</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@stock@</string>
        <key>stock</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>zero</key>
            <string>Out of stock</string>
            <key>one</key>
            <string>Only 1 left in stock</string>
            <key>other</key>
            <string>%d items in stock</string>
        </dict>
    </dict>
    
    <key>product_review_count</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@reviews@</string>
        <key>reviews</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>zero</key>
            <string>No reviews</string>
            <key>one</key>
            <string>1 review</string>
            <key>other</key>
            <string>%d reviews</string>
        </dict>
    </dict>
</dict>
</plist>
Localizable.stringsdict (Korean)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>product_stock_count</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@stock@</string>
        <key>stock</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>zero</key>
            <string>ํ’ˆ์ ˆ</string>
            <key>other</key>
            <string>์žฌ๊ณ  %d๊ฐœ</string>
        </dict>
    </dict>
    
    <key>product_review_count</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@reviews@</string>
        <key>reviews</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>zero</key>
            <string>๋ฆฌ๋ทฐ ์—†์Œ</string>
            <key>other</key>
            <string>๋ฆฌ๋ทฐ %d๊ฐœ</string>
        </dict>
    </dict>
</dict>
</plist>

์ด ์˜ˆ์ œ๋Š” ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์ด์•ผ! ๐ŸŽ‰

ํŠนํžˆ ์ฃผ๋ชฉํ•  ์ :
โœ… ๋ชจ๋“  UI ํ…์ŠคํŠธ๊ฐ€ ์ง€์—ญํ™”๋˜์–ด ์žˆ์–ด
โœ… ๊ฐ€๊ฒฉ์€ ์‚ฌ์šฉ์ž์˜ ๋กœ์ผ€์ผ์— ๋งž๊ฒŒ ํ‘œ์‹œ๋ผ
โœ… ์žฌ๊ณ ์™€ ๋ฆฌ๋ทฐ ์ˆ˜๋Š” ๋ณต์ˆ˜ํ˜• ๊ทœ์น™์ด ์ ์šฉ๋ผ
โœ… Auto Layout์œผ๋กœ RTL ์–ธ์–ด๋„ ์ž๋™ ๋Œ€์‘
โœ… ์ฝ”๋“œ๊ฐ€ ๊น”๋”ํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฌ์›Œ

๐Ÿš€ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด. ๊ณ ์ƒ ๋งŽ์•˜์–ด! ๐Ÿ‘

iOS ๊ตญ์ œํ™”์™€ NSLocalizedString์— ๋Œ€ํ•ด ์ •๋ง ๋งŽ์€ ๊ฑธ ๋ฐฐ์› ์ง€? ์ด์ œ ๋„ˆ๋„ ์ „ ์„ธ๊ณ„ ์‚ฌ์šฉ์ž๋“ค์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์•ฑ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐ŸŒโœจ

๋งˆ์ง€๋ง‰์œผ๋กœ ํ•ต์‹ฌ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ:

๐ŸŽฏ ํ•ต์‹ฌ ์š”์•ฝ 1. ๊ตญ์ œํ™”์˜ ์ค‘์š”์„ฑ
- ๊ธ€๋กœ๋ฒŒ ์‹œ์žฅ ์ง„์ถœ์˜ ํ•„์ˆ˜ ์š”์†Œ
- ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ–ฅ์ƒ
- ๋‹ค์šด๋กœ๋“œ ์ˆ˜ ์ฆ๊ฐ€

2. NSLocalizedString ๊ธฐ๋ณธ
- ๋ฌธ์ž์—ด ์ง€์—ญํ™”์˜ ํ•ต์‹ฌ ๋„๊ตฌ
- Localizable.strings ํŒŒ์ผ ํ™œ์šฉ
- Comment๋กœ ๋ฒˆ์—ญ๊ฐ€ ์ง€์›

3. ๋ณต์ˆ˜ํ˜• ์ฒ˜๋ฆฌ
- Stringsdict ํŒŒ์ผ ์‚ฌ์šฉ
- ์–ธ์–ด๋ณ„ ๋ณต์ˆ˜ํ˜• ๊ทœ์น™ ์ ์šฉ
- String.localizedStringWithFormat ํ™œ์šฉ

4. RTL ์–ธ์–ด ์ง€์›
- Auto Layout ํ™œ์šฉ
- leading/trailing ์‚ฌ์šฉ
- UIStackView๋กœ ๊ฐ„ํŽธํ•˜๊ฒŒ

5. ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค
- ๊ตฌ์กฐํ™”๋œ ํ‚ค ๊ด€๋ฆฌ
- ๋ฒˆ์—ญ ํ’ˆ์งˆ ๊ด€๋ฆฌ
- ์ง€์†์ ์ธ ํ…Œ์ŠคํŠธ

์•ž์œผ๋กœ ์•ฑ์„ ๊ฐœ๋ฐœํ•  ๋•Œ๋Š” ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ตญ์ œํ™”๋ฅผ ์—ผ๋‘์— ๋‘๊ณ  ์„ค๊ณ„ํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ๋‚˜์ค‘์— ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด ์ •๋ง ํž˜๋“ค๊ฑฐ๋“ ! ๐Ÿ˜…

๊ทธ๋ฆฌ๊ณ  ์žฌ๋Šฅ๋„ท(https://www.jaenung.net)์—์„œ ๊ตญ์ œํ™” ๊ด€๋ จ ํ”„๋กœ์ ํŠธ๋ฅผ ๋ฐ›๊ฑฐ๋‚˜, ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ๊ฒฝํ—˜์„ ๊ณต์œ ํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ์‹ค์ „ ๊ฒฝํ—˜์ด ๊ฐ€์žฅ ์ค‘์š”ํ•˜๋‹ˆ๊นŒ! ๐Ÿ’ช

ํ˜น์‹œ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด:
๐Ÿ“š Apple ๊ณต์‹ ๋ฌธ์„œ ์ฐธ๊ณ 
๐Ÿ” Stack Overflow ๊ฒ€์ƒ‰
๐Ÿ‘ฅ ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ ํ™œ์šฉ
๐Ÿ’ก ์‹ค์ œ๋กœ ๋งŒ๋“ค์–ด๋ณด๋ฉฐ ํ•™์Šต

์ž, ์ด์ œ ๋„ˆ๋งŒ์˜ ๋ฉ‹์ง„ ๋‹ค๊ตญ์–ด ์•ฑ์„ ๋งŒ๋“ค์–ด๋ด! ํ™”์ดํŒ…! ๐Ÿš€โœจ

๐Ÿ’š ๋งˆ์ง€๋ง‰ ์กฐ์–ธ ์™„๋ฒฝํ•œ ๋ฒˆ์—ญ๋ณด๋‹ค๋Š” ์ผ๋‹จ ์‹œ์ž‘ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ์ฒ˜์Œ์—๋Š” ์ฃผ์š” ์–ธ์–ด 2-3๊ฐœ๋งŒ ์ง€์›ํ•˜๊ณ , ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์œผ๋ฉด์„œ ์ ์ง„์ ์œผ๋กœ ๊ฐœ์„ ํ•ด๋‚˜๊ฐ€๋ฉด ๋ผ. ์™„๋ฒฝ์ฃผ์˜์— ๋น ์ ธ์„œ ์ถœ์‹œ๋ฅผ ๋ฏธ๋ฃจ์ง€ ๋งˆ! ๐Ÿ˜Š
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.