@agent-native/core 0.75.5 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/docs/AGENTS.md +1 -0
  4. package/corpus/core/docs/content/client.md +15 -0
  5. package/corpus/core/docs/content/internationalization.md +145 -0
  6. package/corpus/core/docs/content/locales/ar-SA/internationalization.md +102 -0
  7. package/corpus/core/docs/content/locales/de-DE/internationalization.md +102 -0
  8. package/corpus/core/docs/content/locales/es-ES/internationalization.md +102 -0
  9. package/corpus/core/docs/content/locales/fr-FR/internationalization.md +102 -0
  10. package/corpus/core/docs/content/locales/hi-IN/internationalization.md +102 -0
  11. package/corpus/core/docs/content/locales/ja-JP/internationalization.md +114 -0
  12. package/corpus/core/docs/content/locales/ko-KR/internationalization.md +102 -0
  13. package/corpus/core/docs/content/locales/pt-BR/internationalization.md +102 -0
  14. package/corpus/core/docs/content/locales/zh-CN/internationalization.md +102 -0
  15. package/corpus/core/docs/content/skills-guide.md +19 -18
  16. package/corpus/core/package.json +7 -1
  17. package/corpus/core/src/client/CommandMenu.tsx +16 -0
  18. package/corpus/core/src/client/app-providers.tsx +26 -1
  19. package/corpus/core/src/client/i18n.tsx +605 -0
  20. package/corpus/core/src/client/index.ts +28 -0
  21. package/corpus/core/src/index.browser.ts +25 -0
  22. package/corpus/core/src/localization/actions/get-localization-preference.ts +23 -0
  23. package/corpus/core/src/localization/actions/set-localization-preference.ts +30 -0
  24. package/corpus/core/src/localization/index.ts +2 -0
  25. package/corpus/core/src/localization/server.ts +131 -0
  26. package/corpus/core/src/localization/shared.ts +163 -0
  27. package/corpus/core/src/server/action-discovery.ts +8 -0
  28. package/corpus/core/src/server/index.ts +8 -0
  29. package/corpus/core/src/templates/default/.agents/skills/adding-a-feature/SKILL.md +5 -0
  30. package/corpus/core/src/templates/default/.agents/skills/internationalization/SKILL.md +77 -0
  31. package/corpus/core/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +6 -0
  32. package/corpus/core/src/templates/default/AGENTS.md +16 -15
  33. package/corpus/core/src/templates/default/app/i18n/en-US.ts +29 -0
  34. package/corpus/core/src/templates/default/app/i18n/index.ts +13 -0
  35. package/corpus/core/src/templates/default/app/root.tsx +20 -1
  36. package/corpus/core/src/templates/default/app/routes/_index.tsx +24 -9
  37. package/corpus/core/src/templates/default/app/routes/settings.tsx +67 -0
  38. package/corpus/core/src/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +5 -0
  39. package/corpus/core/src/templates/workspace-core/.agents/skills/internationalization/SKILL.md +77 -0
  40. package/corpus/core/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +6 -0
  41. package/corpus/core/src/vite/action-types-plugin.ts +10 -0
  42. package/corpus/templates/analytics/.agents/skills/self-modifying-code/SKILL.md +6 -0
  43. package/corpus/templates/analytics/app/components/layout/Header.tsx +18 -15
  44. package/corpus/templates/analytics/app/components/layout/Layout.tsx +7 -5
  45. package/corpus/templates/analytics/app/components/layout/MobileNav.tsx +9 -3
  46. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +5 -3
  47. package/corpus/templates/analytics/app/i18n/ar-SA.ts +3 -0
  48. package/corpus/templates/analytics/app/i18n/de-DE.ts +3 -0
  49. package/corpus/templates/analytics/app/i18n/en-US.ts +3 -0
  50. package/corpus/templates/analytics/app/i18n/es-ES.ts +3 -0
  51. package/corpus/templates/analytics/app/i18n/fr-FR.ts +3 -0
  52. package/corpus/templates/analytics/app/i18n/hi-IN.ts +3 -0
  53. package/corpus/templates/analytics/app/i18n/index.ts +31 -0
  54. package/corpus/templates/analytics/app/i18n/ja-JP.ts +3 -0
  55. package/corpus/templates/analytics/app/i18n/ko-KR.ts +3 -0
  56. package/corpus/templates/analytics/app/i18n/pt-BR.ts +3 -0
  57. package/corpus/templates/analytics/app/i18n/zh-CN.ts +3 -0
  58. package/corpus/templates/analytics/app/i18n-data.ts +433 -0
  59. package/corpus/templates/analytics/app/pages/Settings.tsx +50 -16
  60. package/corpus/templates/analytics/app/root.tsx +14 -1
  61. package/corpus/templates/analytics/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  62. package/corpus/templates/analytics/changelog/2026-06-24-settings-now-link-directly-to-agent-settings-for-model-api-k.md +6 -0
  63. package/corpus/templates/assets/.agents/skills/self-modifying-code/SKILL.md +6 -0
  64. package/corpus/templates/assets/app/components/layout/Header.tsx +17 -8
  65. package/corpus/templates/assets/app/components/layout/Layout.tsx +10 -6
  66. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +40 -23
  67. package/corpus/templates/assets/app/i18n/ar-SA.ts +3 -0
  68. package/corpus/templates/assets/app/i18n/de-DE.ts +3 -0
  69. package/corpus/templates/assets/app/i18n/en-US.ts +3 -0
  70. package/corpus/templates/assets/app/i18n/es-ES.ts +3 -0
  71. package/corpus/templates/assets/app/i18n/fr-FR.ts +3 -0
  72. package/corpus/templates/assets/app/i18n/hi-IN.ts +3 -0
  73. package/corpus/templates/assets/app/i18n/index.ts +31 -0
  74. package/corpus/templates/assets/app/i18n/ja-JP.ts +3 -0
  75. package/corpus/templates/assets/app/i18n/ko-KR.ts +3 -0
  76. package/corpus/templates/assets/app/i18n/pt-BR.ts +3 -0
  77. package/corpus/templates/assets/app/i18n/zh-CN.ts +3 -0
  78. package/corpus/templates/assets/app/i18n-data.ts +668 -0
  79. package/corpus/templates/assets/app/root.tsx +40 -15
  80. package/corpus/templates/assets/app/routes/_index.tsx +9 -10
  81. package/corpus/templates/assets/app/routes/settings.tsx +37 -6
  82. package/corpus/templates/assets/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  83. package/corpus/templates/assets/changelog/2026-06-24-settings-now-link-directly-to-agent-settings-for-model-api-k.md +6 -0
  84. package/corpus/templates/brain/.agents/skills/adding-a-feature/SKILL.md +5 -0
  85. package/corpus/templates/brain/.agents/skills/self-modifying-code/SKILL.md +6 -0
  86. package/corpus/templates/brain/app/components/layout/Layout.tsx +12 -8
  87. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +23 -12
  88. package/corpus/templates/brain/app/i18n/ar-SA.ts +3 -0
  89. package/corpus/templates/brain/app/i18n/de-DE.ts +3 -0
  90. package/corpus/templates/brain/app/i18n/en-US.ts +3 -0
  91. package/corpus/templates/brain/app/i18n/es-ES.ts +3 -0
  92. package/corpus/templates/brain/app/i18n/fr-FR.ts +3 -0
  93. package/corpus/templates/brain/app/i18n/hi-IN.ts +3 -0
  94. package/corpus/templates/brain/app/i18n/index.ts +31 -0
  95. package/corpus/templates/brain/app/i18n/ja-JP.ts +3 -0
  96. package/corpus/templates/brain/app/i18n/ko-KR.ts +3 -0
  97. package/corpus/templates/brain/app/i18n/pt-BR.ts +3 -0
  98. package/corpus/templates/brain/app/i18n/zh-CN.ts +3 -0
  99. package/corpus/templates/brain/app/i18n-data.ts +624 -0
  100. package/corpus/templates/brain/app/root.tsx +27 -12
  101. package/corpus/templates/brain/app/routes/settings.tsx +45 -8
  102. package/corpus/templates/brain/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  103. package/corpus/templates/brain/changelog/2026-06-24-settings-now-link-directly-to-agent-settings-for-model-api-k.md +6 -0
  104. package/corpus/templates/calendar/.agents/skills/self-modifying-code/SKILL.md +6 -0
  105. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +2 -0
  106. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +16 -9
  107. package/corpus/templates/calendar/app/i18n/ar-SA.ts +57 -0
  108. package/corpus/templates/calendar/app/i18n/de-DE.ts +58 -0
  109. package/corpus/templates/calendar/app/i18n/en-US.ts +57 -0
  110. package/corpus/templates/calendar/app/i18n/es-ES.ts +58 -0
  111. package/corpus/templates/calendar/app/i18n/fr-FR.ts +58 -0
  112. package/corpus/templates/calendar/app/i18n/hi-IN.ts +57 -0
  113. package/corpus/templates/calendar/app/i18n/index.ts +31 -0
  114. package/corpus/templates/calendar/app/i18n/ja-JP.ts +58 -0
  115. package/corpus/templates/calendar/app/i18n/ko-KR.ts +57 -0
  116. package/corpus/templates/calendar/app/i18n/pt-BR.ts +57 -0
  117. package/corpus/templates/calendar/app/i18n/zh-CN.ts +57 -0
  118. package/corpus/templates/calendar/app/i18n-data.ts +516 -0
  119. package/corpus/templates/calendar/app/pages/CalendarView.tsx +2 -0
  120. package/corpus/templates/calendar/app/pages/Settings.tsx +63 -28
  121. package/corpus/templates/calendar/app/root.tsx +78 -7
  122. package/corpus/templates/calendar/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  123. package/corpus/templates/calendar/changelog/2026-06-24-settings-now-link-directly-to-agent-settings-for-model-api-k.md +6 -0
  124. package/corpus/templates/chat/.agents/skills/adding-a-feature/SKILL.md +5 -0
  125. package/corpus/templates/chat/.agents/skills/self-modifying-code/SKILL.md +6 -0
  126. package/corpus/templates/chat/app/components/layout/Header.tsx +17 -11
  127. package/corpus/templates/chat/app/components/layout/Layout.tsx +12 -8
  128. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +47 -18
  129. package/corpus/templates/chat/app/i18n/ar-SA.ts +62 -0
  130. package/corpus/templates/chat/app/i18n/de-DE.ts +63 -0
  131. package/corpus/templates/chat/app/i18n/en-US.ts +63 -0
  132. package/corpus/templates/chat/app/i18n/es-ES.ts +63 -0
  133. package/corpus/templates/chat/app/i18n/fr-FR.ts +63 -0
  134. package/corpus/templates/chat/app/i18n/hi-IN.ts +63 -0
  135. package/corpus/templates/chat/app/i18n/index.ts +31 -0
  136. package/corpus/templates/chat/app/i18n/ja-JP.ts +63 -0
  137. package/corpus/templates/chat/app/i18n/ko-KR.ts +63 -0
  138. package/corpus/templates/chat/app/i18n/pt-BR.ts +63 -0
  139. package/corpus/templates/chat/app/i18n/zh-CN.ts +61 -0
  140. package/corpus/templates/chat/app/i18n-data.ts +475 -0
  141. package/corpus/templates/chat/app/root.tsx +18 -5
  142. package/corpus/templates/chat/app/routes/_index.tsx +9 -8
  143. package/corpus/templates/chat/app/routes/settings.tsx +86 -0
  144. package/corpus/templates/chat/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +6 -0
  145. package/corpus/templates/chat/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  146. package/corpus/templates/clips/.agents/skills/self-modifying-code/SKILL.md +6 -0
  147. package/corpus/templates/clips/app/components/layout/Header.tsx +23 -17
  148. package/corpus/templates/clips/app/components/library/empty-state.tsx +10 -34
  149. package/corpus/templates/clips/app/components/library/library-layout.tsx +45 -34
  150. package/corpus/templates/clips/app/i18n/ar-SA.ts +182 -0
  151. package/corpus/templates/clips/app/i18n/de-DE.ts +184 -0
  152. package/corpus/templates/clips/app/i18n/en-US.ts +179 -0
  153. package/corpus/templates/clips/app/i18n/es-ES.ts +183 -0
  154. package/corpus/templates/clips/app/i18n/fr-FR.ts +184 -0
  155. package/corpus/templates/clips/app/i18n/hi-IN.ts +180 -0
  156. package/corpus/templates/clips/app/i18n/index.ts +41 -0
  157. package/corpus/templates/clips/app/i18n/ja-JP.ts +180 -0
  158. package/corpus/templates/clips/app/i18n/ko-KR.ts +178 -0
  159. package/corpus/templates/clips/app/i18n/pt-BR.ts +183 -0
  160. package/corpus/templates/clips/app/i18n/zh-CN.ts +173 -0
  161. package/corpus/templates/clips/app/root.tsx +104 -10
  162. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +154 -95
  163. package/corpus/templates/clips/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  164. package/corpus/templates/clips/changelog/2026-06-24-clips-now-includes-a-language-picker-and-simplified-chinese-ui.md +6 -0
  165. package/corpus/templates/clips/changelog/2026-06-24-settings-now-link-directly-to-agent-settings-for-model-api-k.md +6 -0
  166. package/corpus/templates/content/.agents/skills/self-modifying-code/SKILL.md +6 -0
  167. package/corpus/templates/content/app/components/EmptyState.tsx +7 -5
  168. package/corpus/templates/content/app/components/ThemeToggle.tsx +4 -2
  169. package/corpus/templates/content/app/components/layout/Header.tsx +2 -0
  170. package/corpus/templates/content/app/components/layout/Layout.tsx +8 -7
  171. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +41 -1
  172. package/corpus/templates/content/app/i18n/ar-SA.ts +3 -0
  173. package/corpus/templates/content/app/i18n/de-DE.ts +3 -0
  174. package/corpus/templates/content/app/i18n/en-US.ts +3 -0
  175. package/corpus/templates/content/app/i18n/es-ES.ts +3 -0
  176. package/corpus/templates/content/app/i18n/fr-FR.ts +3 -0
  177. package/corpus/templates/content/app/i18n/hi-IN.ts +3 -0
  178. package/corpus/templates/content/app/i18n/index.ts +31 -0
  179. package/corpus/templates/content/app/i18n/ja-JP.ts +3 -0
  180. package/corpus/templates/content/app/i18n/ko-KR.ts +3 -0
  181. package/corpus/templates/content/app/i18n/pt-BR.ts +3 -0
  182. package/corpus/templates/content/app/i18n/zh-CN.ts +3 -0
  183. package/corpus/templates/content/app/i18n-data.ts +490 -0
  184. package/corpus/templates/content/app/root.tsx +110 -23
  185. package/corpus/templates/content/app/routes/_app.settings.tsx +84 -0
  186. package/corpus/templates/content/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +6 -0
  187. package/corpus/templates/content/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  188. package/corpus/templates/design/.agents/skills/self-modifying-code/SKILL.md +6 -0
  189. package/corpus/templates/design/app/components/layout/Header.tsx +16 -8
  190. package/corpus/templates/design/app/components/layout/Layout.tsx +8 -6
  191. package/corpus/templates/design/app/components/layout/Sidebar.tsx +25 -9
  192. package/corpus/templates/design/app/i18n/ar-SA.ts +3 -0
  193. package/corpus/templates/design/app/i18n/de-DE.ts +3 -0
  194. package/corpus/templates/design/app/i18n/en-US.ts +3 -0
  195. package/corpus/templates/design/app/i18n/es-ES.ts +3 -0
  196. package/corpus/templates/design/app/i18n/fr-FR.ts +3 -0
  197. package/corpus/templates/design/app/i18n/hi-IN.ts +3 -0
  198. package/corpus/templates/design/app/i18n/index.ts +31 -0
  199. package/corpus/templates/design/app/i18n/ja-JP.ts +3 -0
  200. package/corpus/templates/design/app/i18n/ko-KR.ts +3 -0
  201. package/corpus/templates/design/app/i18n/pt-BR.ts +3 -0
  202. package/corpus/templates/design/app/i18n/zh-CN.ts +3 -0
  203. package/corpus/templates/design/app/i18n-data.ts +350 -0
  204. package/corpus/templates/design/app/root.tsx +40 -15
  205. package/corpus/templates/design/app/routes/settings.tsx +43 -0
  206. package/corpus/templates/design/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  207. package/corpus/templates/design/changelog/2026-06-24-settings-now-link-directly-to-agent-settings-for-model-api-k.md +6 -0
  208. package/corpus/templates/dispatch/.agents/skills/self-modifying-code/SKILL.md +6 -0
  209. package/corpus/templates/dispatch/app/i18n/ar-SA.ts +3 -0
  210. package/corpus/templates/dispatch/app/i18n/de-DE.ts +3 -0
  211. package/corpus/templates/dispatch/app/i18n/en-US.ts +3 -0
  212. package/corpus/templates/dispatch/app/i18n/es-ES.ts +3 -0
  213. package/corpus/templates/dispatch/app/i18n/fr-FR.ts +3 -0
  214. package/corpus/templates/dispatch/app/i18n/hi-IN.ts +3 -0
  215. package/corpus/templates/dispatch/app/i18n/index.ts +31 -0
  216. package/corpus/templates/dispatch/app/i18n/ja-JP.ts +3 -0
  217. package/corpus/templates/dispatch/app/i18n/ko-KR.ts +3 -0
  218. package/corpus/templates/dispatch/app/i18n/pt-BR.ts +3 -0
  219. package/corpus/templates/dispatch/app/i18n/zh-CN.ts +3 -0
  220. package/corpus/templates/dispatch/app/i18n-data.ts +267 -0
  221. package/corpus/templates/dispatch/app/root.tsx +18 -4
  222. package/corpus/templates/dispatch/app/routes/settings.tsx +86 -0
  223. package/corpus/templates/dispatch/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +6 -0
  224. package/corpus/templates/dispatch/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  225. package/corpus/templates/forms/.agents/skills/self-modifying-code/SKILL.md +6 -0
  226. package/corpus/templates/forms/app/components/layout/Header.tsx +16 -9
  227. package/corpus/templates/forms/app/components/layout/Layout.tsx +6 -4
  228. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +34 -17
  229. package/corpus/templates/forms/app/i18n/ar-SA.ts +58 -0
  230. package/corpus/templates/forms/app/i18n/de-DE.ts +59 -0
  231. package/corpus/templates/forms/app/i18n/en-US.ts +59 -0
  232. package/corpus/templates/forms/app/i18n/es-ES.ts +59 -0
  233. package/corpus/templates/forms/app/i18n/fr-FR.ts +59 -0
  234. package/corpus/templates/forms/app/i18n/hi-IN.ts +59 -0
  235. package/corpus/templates/forms/app/i18n/index.ts +31 -0
  236. package/corpus/templates/forms/app/i18n/ja-JP.ts +59 -0
  237. package/corpus/templates/forms/app/i18n/ko-KR.ts +59 -0
  238. package/corpus/templates/forms/app/i18n/pt-BR.ts +59 -0
  239. package/corpus/templates/forms/app/i18n/zh-CN.ts +56 -0
  240. package/corpus/templates/forms/app/root.tsx +65 -16
  241. package/corpus/templates/forms/app/routes/_app.settings.tsx +85 -0
  242. package/corpus/templates/forms/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +6 -0
  243. package/corpus/templates/forms/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  244. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +29 -17
  245. package/corpus/templates/macros/app/components/layout/Header.tsx +16 -10
  246. package/corpus/templates/macros/app/i18n/ar-SA.ts +54 -0
  247. package/corpus/templates/macros/app/i18n/de-DE.ts +55 -0
  248. package/corpus/templates/macros/app/i18n/en-US.ts +55 -0
  249. package/corpus/templates/macros/app/i18n/es-ES.ts +55 -0
  250. package/corpus/templates/macros/app/i18n/fr-FR.ts +55 -0
  251. package/corpus/templates/macros/app/i18n/hi-IN.ts +55 -0
  252. package/corpus/templates/macros/app/i18n/index.ts +31 -0
  253. package/corpus/templates/macros/app/i18n/ja-JP.ts +55 -0
  254. package/corpus/templates/macros/app/i18n/ko-KR.ts +55 -0
  255. package/corpus/templates/macros/app/i18n/pt-BR.ts +55 -0
  256. package/corpus/templates/macros/app/i18n/zh-CN.ts +53 -0
  257. package/corpus/templates/macros/app/root.tsx +65 -15
  258. package/corpus/templates/macros/app/routes/settings.tsx +85 -0
  259. package/corpus/templates/macros/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +6 -0
  260. package/corpus/templates/macros/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  261. package/corpus/templates/mail/.agents/skills/self-modifying-code/SKILL.md +6 -0
  262. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +9 -4
  263. package/corpus/templates/mail/app/i18n/ar-SA.ts +33 -0
  264. package/corpus/templates/mail/app/i18n/de-DE.ts +33 -0
  265. package/corpus/templates/mail/app/i18n/en-US.ts +32 -0
  266. package/corpus/templates/mail/app/i18n/es-ES.ts +33 -0
  267. package/corpus/templates/mail/app/i18n/fr-FR.ts +33 -0
  268. package/corpus/templates/mail/app/i18n/hi-IN.ts +32 -0
  269. package/corpus/templates/mail/app/i18n/index.ts +31 -0
  270. package/corpus/templates/mail/app/i18n/ja-JP.ts +32 -0
  271. package/corpus/templates/mail/app/i18n/ko-KR.ts +33 -0
  272. package/corpus/templates/mail/app/i18n/pt-BR.ts +34 -0
  273. package/corpus/templates/mail/app/i18n/zh-CN.ts +32 -0
  274. package/corpus/templates/mail/app/pages/SettingsPage.tsx +67 -13
  275. package/corpus/templates/mail/app/root.tsx +20 -0
  276. package/corpus/templates/mail/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  277. package/corpus/templates/mail/changelog/2026-06-24-settings-now-link-directly-to-agent-settings-for-model-api-k.md +6 -0
  278. package/corpus/templates/plan/.agents/skills/adding-a-feature/SKILL.md +5 -0
  279. package/corpus/templates/plan/.agents/skills/self-modifying-code/SKILL.md +6 -0
  280. package/corpus/templates/plan/app/components/layout/Header.tsx +19 -12
  281. package/corpus/templates/plan/app/components/layout/Layout.tsx +11 -7
  282. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +41 -28
  283. package/corpus/templates/plan/app/i18n/ar-SA.ts +77 -0
  284. package/corpus/templates/plan/app/i18n/de-DE.ts +78 -0
  285. package/corpus/templates/plan/app/i18n/en-US.ts +78 -0
  286. package/corpus/templates/plan/app/i18n/es-ES.ts +78 -0
  287. package/corpus/templates/plan/app/i18n/fr-FR.ts +78 -0
  288. package/corpus/templates/plan/app/i18n/hi-IN.ts +78 -0
  289. package/corpus/templates/plan/app/i18n/index.ts +31 -0
  290. package/corpus/templates/plan/app/i18n/ja-JP.ts +79 -0
  291. package/corpus/templates/plan/app/i18n/ko-KR.ts +77 -0
  292. package/corpus/templates/plan/app/i18n/pt-BR.ts +78 -0
  293. package/corpus/templates/plan/app/i18n/zh-CN.ts +74 -0
  294. package/corpus/templates/plan/app/root.tsx +45 -8
  295. package/corpus/templates/plan/app/routes/settings.tsx +86 -0
  296. package/corpus/templates/plan/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +6 -0
  297. package/corpus/templates/plan/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  298. package/corpus/templates/slides/.agents/skills/self-modifying-code/SKILL.md +6 -0
  299. package/corpus/templates/slides/app/components/layout/Header.tsx +23 -11
  300. package/corpus/templates/slides/app/components/layout/Layout.tsx +7 -6
  301. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +23 -11
  302. package/corpus/templates/slides/app/i18n/ar-SA.ts +54 -0
  303. package/corpus/templates/slides/app/i18n/de-DE.ts +56 -0
  304. package/corpus/templates/slides/app/i18n/en-US.ts +55 -0
  305. package/corpus/templates/slides/app/i18n/es-ES.ts +55 -0
  306. package/corpus/templates/slides/app/i18n/fr-FR.ts +55 -0
  307. package/corpus/templates/slides/app/i18n/hi-IN.ts +55 -0
  308. package/corpus/templates/slides/app/i18n/index.ts +31 -0
  309. package/corpus/templates/slides/app/i18n/ja-JP.ts +55 -0
  310. package/corpus/templates/slides/app/i18n/ko-KR.ts +55 -0
  311. package/corpus/templates/slides/app/i18n/pt-BR.ts +55 -0
  312. package/corpus/templates/slides/app/i18n/zh-CN.ts +53 -0
  313. package/corpus/templates/slides/app/root.tsx +50 -8
  314. package/corpus/templates/slides/app/routes/settings.tsx +85 -0
  315. package/corpus/templates/slides/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +6 -0
  316. package/corpus/templates/slides/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  317. package/corpus/templates/videos/.agents/skills/self-modifying-code/SKILL.md +6 -0
  318. package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +14 -9
  319. package/corpus/templates/videos/app/components/Sidebar.tsx +16 -9
  320. package/corpus/templates/videos/app/components/StudioHeader.tsx +12 -8
  321. package/corpus/templates/videos/app/components/layout/Header.tsx +21 -15
  322. package/corpus/templates/videos/app/components/layout/Layout.tsx +10 -7
  323. package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +21 -6
  324. package/corpus/templates/videos/app/i18n/ar-SA.ts +73 -0
  325. package/corpus/templates/videos/app/i18n/de-DE.ts +75 -0
  326. package/corpus/templates/videos/app/i18n/en-US.ts +74 -0
  327. package/corpus/templates/videos/app/i18n/es-ES.ts +75 -0
  328. package/corpus/templates/videos/app/i18n/fr-FR.ts +75 -0
  329. package/corpus/templates/videos/app/i18n/hi-IN.ts +74 -0
  330. package/corpus/templates/videos/app/i18n/index.ts +31 -0
  331. package/corpus/templates/videos/app/i18n/ja-JP.ts +74 -0
  332. package/corpus/templates/videos/app/i18n/ko-KR.ts +74 -0
  333. package/corpus/templates/videos/app/i18n/pt-BR.ts +75 -0
  334. package/corpus/templates/videos/app/i18n/zh-CN.ts +72 -0
  335. package/corpus/templates/videos/app/pages/Index.tsx +11 -9
  336. package/corpus/templates/videos/app/root.tsx +45 -7
  337. package/corpus/templates/videos/app/routes/settings.tsx +85 -0
  338. package/corpus/templates/videos/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +6 -0
  339. package/corpus/templates/videos/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  340. package/dist/client/CommandMenu.d.ts +3 -0
  341. package/dist/client/CommandMenu.d.ts.map +1 -1
  342. package/dist/client/CommandMenu.js +9 -0
  343. package/dist/client/CommandMenu.js.map +1 -1
  344. package/dist/client/app-providers.d.ts +8 -1
  345. package/dist/client/app-providers.d.ts.map +1 -1
  346. package/dist/client/app-providers.js +7 -5
  347. package/dist/client/app-providers.js.map +1 -1
  348. package/dist/client/i18n.d.ts +54 -0
  349. package/dist/client/i18n.d.ts.map +1 -0
  350. package/dist/client/i18n.js +354 -0
  351. package/dist/client/i18n.js.map +1 -0
  352. package/dist/client/index.d.ts +2 -1
  353. package/dist/client/index.d.ts.map +1 -1
  354. package/dist/client/index.js +2 -1
  355. package/dist/client/index.js.map +1 -1
  356. package/dist/index.browser.d.ts +1 -1
  357. package/dist/index.browser.d.ts.map +1 -1
  358. package/dist/index.browser.js +1 -1
  359. package/dist/index.browser.js.map +1 -1
  360. package/dist/localization/actions/get-localization-preference.d.ts +4 -0
  361. package/dist/localization/actions/get-localization-preference.d.ts.map +1 -0
  362. package/dist/localization/actions/get-localization-preference.js +16 -0
  363. package/dist/localization/actions/get-localization-preference.js.map +1 -0
  364. package/dist/localization/actions/set-localization-preference.d.ts +6 -0
  365. package/dist/localization/actions/set-localization-preference.d.ts.map +1 -0
  366. package/dist/localization/actions/set-localization-preference.js +24 -0
  367. package/dist/localization/actions/set-localization-preference.js.map +1 -0
  368. package/dist/localization/index.d.ts +3 -0
  369. package/dist/localization/index.d.ts.map +1 -0
  370. package/dist/localization/index.js +3 -0
  371. package/dist/localization/index.js.map +1 -0
  372. package/dist/localization/server.d.ts +25 -0
  373. package/dist/localization/server.d.ts.map +1 -0
  374. package/dist/localization/server.js +69 -0
  375. package/dist/localization/server.js.map +1 -0
  376. package/dist/localization/shared.d.ts +25 -0
  377. package/dist/localization/shared.d.ts.map +1 -0
  378. package/dist/localization/shared.js +134 -0
  379. package/dist/localization/shared.js.map +1 -0
  380. package/dist/server/action-discovery.d.ts.map +1 -1
  381. package/dist/server/action-discovery.js +8 -0
  382. package/dist/server/action-discovery.js.map +1 -1
  383. package/dist/server/index.d.ts +1 -0
  384. package/dist/server/index.d.ts.map +1 -1
  385. package/dist/server/index.js +1 -0
  386. package/dist/server/index.js.map +1 -1
  387. package/dist/templates/default/.agents/skills/adding-a-feature/SKILL.md +5 -0
  388. package/dist/templates/default/.agents/skills/internationalization/SKILL.md +77 -0
  389. package/dist/templates/default/.agents/skills/self-modifying-code/SKILL.md +6 -0
  390. package/dist/templates/default/AGENTS.md +16 -15
  391. package/dist/templates/default/app/i18n/en-US.ts +29 -0
  392. package/dist/templates/default/app/i18n/index.ts +13 -0
  393. package/dist/templates/default/app/root.tsx +20 -1
  394. package/dist/templates/default/app/routes/_index.tsx +24 -9
  395. package/dist/templates/default/app/routes/settings.tsx +67 -0
  396. package/dist/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +5 -0
  397. package/dist/templates/workspace-core/.agents/skills/internationalization/SKILL.md +77 -0
  398. package/dist/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +6 -0
  399. package/dist/vite/action-types-plugin.d.ts.map +1 -1
  400. package/dist/vite/action-types-plugin.js +8 -0
  401. package/dist/vite/action-types-plugin.js.map +1 -1
  402. package/docs/AGENTS.md +1 -0
  403. package/docs/content/client.md +15 -0
  404. package/docs/content/internationalization.md +145 -0
  405. package/docs/content/locales/ar-SA/internationalization.md +102 -0
  406. package/docs/content/locales/de-DE/internationalization.md +102 -0
  407. package/docs/content/locales/es-ES/internationalization.md +102 -0
  408. package/docs/content/locales/fr-FR/internationalization.md +102 -0
  409. package/docs/content/locales/hi-IN/internationalization.md +102 -0
  410. package/docs/content/locales/ja-JP/internationalization.md +114 -0
  411. package/docs/content/locales/ko-KR/internationalization.md +102 -0
  412. package/docs/content/locales/pt-BR/internationalization.md +102 -0
  413. package/docs/content/locales/zh-CN/internationalization.md +102 -0
  414. package/docs/content/skills-guide.md +19 -18
  415. package/package.json +7 -1
  416. package/src/templates/default/.agents/skills/adding-a-feature/SKILL.md +5 -0
  417. package/src/templates/default/.agents/skills/internationalization/SKILL.md +77 -0
  418. package/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +6 -0
  419. package/src/templates/default/AGENTS.md +16 -15
  420. package/src/templates/default/app/i18n/en-US.ts +29 -0
  421. package/src/templates/default/app/i18n/index.ts +13 -0
  422. package/src/templates/default/app/root.tsx +20 -1
  423. package/src/templates/default/app/routes/_index.tsx +24 -9
  424. package/src/templates/default/app/routes/settings.tsx +67 -0
  425. package/src/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +5 -0
  426. package/src/templates/workspace-core/.agents/skills/internationalization/SKILL.md +77 -0
  427. package/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +6 -0
@@ -53,6 +53,10 @@ import { TooltipProvider } from "@radix-ui/react-tooltip";
53
53
  import { Toaster } from "sonner";
54
54
  import { ClientOnly } from "./ClientOnly.js";
55
55
  import { DefaultSpinner } from "./DefaultSpinner.js";
56
+ import {
57
+ AgentNativeI18nProvider,
58
+ type AgentNativeI18nProviderProps,
59
+ } from "./i18n.js";
56
60
 
57
61
  export interface AppProvidersProps {
58
62
  /** QueryClient instance — create with `createAgentNativeQueryClient()`. */
@@ -94,6 +98,13 @@ export interface AppProvidersProps {
94
98
  */
95
99
  disableThemeTransitions?: boolean;
96
100
 
101
+ /**
102
+ * Optional localization runtime configuration. When omitted, AppProviders
103
+ * still mounts the i18n provider with an English fallback so templates can
104
+ * call useT/useLocale before they add catalogs. Pass false to opt out.
105
+ */
106
+ i18n?: Omit<AgentNativeI18nProviderProps, "children"> | false;
107
+
97
108
  /**
98
109
  * When true the providers render without a `<ClientOnly>` gate so SSR
99
110
  * streams real markup for public/unauthenticated paths.
@@ -119,6 +130,7 @@ function ProvidersInner({
119
130
  tooltipDelayDuration,
120
131
  toaster = DEFAULT_TOASTER,
121
132
  disableThemeTransitions = true,
133
+ i18n,
122
134
  children,
123
135
  }: {
124
136
  queryClient: QueryClient;
@@ -127,8 +139,18 @@ function ProvidersInner({
127
139
  tooltipDelayDuration?: number;
128
140
  toaster?: React.ReactNode | null;
129
141
  disableThemeTransitions?: boolean;
142
+ i18n?: Omit<AgentNativeI18nProviderProps, "children"> | false;
130
143
  children: React.ReactNode;
131
144
  }) {
145
+ const localizedChildren =
146
+ i18n === false ? (
147
+ children
148
+ ) : (
149
+ <AgentNativeI18nProvider {...(i18n ?? {})}>
150
+ {children}
151
+ </AgentNativeI18nProvider>
152
+ );
153
+
132
154
  return (
133
155
  <QueryClientProvider client={queryClient}>
134
156
  <ThemeProvider
@@ -138,7 +160,7 @@ function ProvidersInner({
138
160
  disableTransitionOnChange={disableThemeTransitions}
139
161
  >
140
162
  <TooltipProvider delayDuration={tooltipDelayDuration}>
141
- {children}
163
+ {localizedChildren}
142
164
  {toaster}
143
165
  </TooltipProvider>
144
166
  </ThemeProvider>
@@ -155,6 +177,7 @@ export function AppProviders({
155
177
  tooltipDelayDuration,
156
178
  toaster,
157
179
  disableThemeTransitions,
180
+ i18n,
158
181
  children,
159
182
  }: AppProvidersProps) {
160
183
  const fallback = clientOnlyFallback ?? <DefaultSpinner />;
@@ -168,6 +191,7 @@ export function AppProviders({
168
191
  tooltipDelayDuration={tooltipDelayDuration}
169
192
  toaster={toaster}
170
193
  disableThemeTransitions={disableThemeTransitions}
194
+ i18n={i18n}
171
195
  >
172
196
  {children}
173
197
  </ProvidersInner>
@@ -183,6 +207,7 @@ export function AppProviders({
183
207
  tooltipDelayDuration={tooltipDelayDuration}
184
208
  toaster={toaster}
185
209
  disableThemeTransitions={disableThemeTransitions}
210
+ i18n={i18n}
186
211
  >
187
212
  {children}
188
213
  </ProvidersInner>
@@ -0,0 +1,605 @@
1
+ import * as SelectPrimitive from "@radix-ui/react-select";
2
+ import { IconCheck, IconChevronDown, IconLanguage } from "@tabler/icons-react";
3
+ import i18next, { type i18n as I18nInstance } from "i18next";
4
+ import React, {
5
+ createContext,
6
+ useCallback,
7
+ useContext,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from "react";
13
+ import {
14
+ I18nextProvider,
15
+ initReactI18next,
16
+ useTranslation,
17
+ } from "react-i18next";
18
+ import { callAction } from "./use-action.js";
19
+ import { setClientAppState } from "./application-state.js";
20
+ import {
21
+ DEFAULT_LOCALE,
22
+ LOCALE_HYDRATION_GLOBAL,
23
+ LOCALE_METADATA,
24
+ LOCALE_STORAGE_KEY,
25
+ SUPPORTED_LOCALES,
26
+ localeDirection,
27
+ normalizeLocalizationPreference,
28
+ resolveLocaleFromCandidates,
29
+ resolveLocaleFromPreference,
30
+ type LocaleCode,
31
+ type LocaleMetadata,
32
+ type LocalePreference,
33
+ type LocalizationPreference,
34
+ } from "../localization/shared.js";
35
+
36
+ export {
37
+ DEFAULT_LOCALE,
38
+ LOCALE_HYDRATION_GLOBAL,
39
+ LOCALE_METADATA,
40
+ LOCALE_STORAGE_KEY,
41
+ SUPPORTED_LOCALES,
42
+ localeDirection,
43
+ normalizeLocaleCode,
44
+ normalizeLocalePreference,
45
+ normalizeLocalizationPreference,
46
+ resolveLocaleFromCandidates,
47
+ resolveLocaleFromPreference,
48
+ type LocaleCode,
49
+ type LocaleMetadata,
50
+ type LocalePreference,
51
+ type LocalizationPreference,
52
+ } from "../localization/shared.js";
53
+ export { getLocaleInitScript } from "../localization/server.js";
54
+
55
+ export type LocaleMessages = Record<string, unknown>;
56
+
57
+ export interface LocaleHydrationPayload {
58
+ locale?: LocaleCode;
59
+ preference?: LocalizationPreference;
60
+ dir?: "ltr" | "rtl";
61
+ messages?: LocaleMessages;
62
+ }
63
+
64
+ export interface AgentNativeI18nCatalog {
65
+ namespace?: string;
66
+ sourceLocale?: LocaleCode;
67
+ messages?: LocaleMessages;
68
+ loadMessages?: (locale: LocaleCode) => Promise<LocaleMessages | null>;
69
+ }
70
+
71
+ export interface AgentNativeI18nProviderProps {
72
+ children: React.ReactNode;
73
+ catalog?: AgentNativeI18nCatalog;
74
+ initialLocale?: LocaleCode;
75
+ initialPreference?: LocalizationPreference | LocalePreference;
76
+ initialMessages?: LocaleMessages | null;
77
+ persistPreference?: boolean;
78
+ }
79
+
80
+ interface LocaleContextValue {
81
+ locale: LocaleCode;
82
+ preference: LocalePreference;
83
+ dir: "ltr" | "rtl";
84
+ metadata: LocaleMetadata;
85
+ setPreference: (preference: LocalePreference) => Promise<void>;
86
+ loading: boolean;
87
+ }
88
+
89
+ declare global {
90
+ interface Window {
91
+ __AGENT_NATIVE_LOCALE__?: LocaleHydrationPayload;
92
+ }
93
+ }
94
+
95
+ const LocaleContext = createContext<LocaleContextValue | null>(null);
96
+
97
+ const LANGUAGE_PICKER_COPY: Record<
98
+ LocaleCode,
99
+ { label: string; system: string; systemDescription: string }
100
+ > = {
101
+ "en-US": {
102
+ label: "Language",
103
+ system: "System",
104
+ systemDescription: "Use your browser language",
105
+ },
106
+ "zh-CN": {
107
+ label: "语言",
108
+ system: "系统",
109
+ systemDescription: "使用浏览器语言",
110
+ },
111
+ "es-ES": {
112
+ label: "Idioma",
113
+ system: "Sistema",
114
+ systemDescription: "Usar el idioma del navegador",
115
+ },
116
+ "fr-FR": {
117
+ label: "Langue",
118
+ system: "Système",
119
+ systemDescription: "Utiliser la langue du navigateur",
120
+ },
121
+ "de-DE": {
122
+ label: "Sprache",
123
+ system: "System",
124
+ systemDescription: "Browsersprache verwenden",
125
+ },
126
+ "ja-JP": {
127
+ label: "言語",
128
+ system: "システム",
129
+ systemDescription: "ブラウザーの言語を使用",
130
+ },
131
+ "ko-KR": {
132
+ label: "언어",
133
+ system: "시스템",
134
+ systemDescription: "브라우저 언어 사용",
135
+ },
136
+ "pt-BR": {
137
+ label: "Idioma",
138
+ system: "Sistema",
139
+ systemDescription: "Usar o idioma do navegador",
140
+ },
141
+ "hi-IN": {
142
+ label: "भाषा",
143
+ system: "सिस्टम",
144
+ systemDescription: "ब्राउज़र की भाषा का उपयोग करें",
145
+ },
146
+ "ar-SA": {
147
+ label: "اللغة",
148
+ system: "النظام",
149
+ systemDescription: "استخدام لغة المتصفح",
150
+ },
151
+ };
152
+
153
+ function browserLanguageCandidates(): string[] {
154
+ if (typeof navigator === "undefined") return [];
155
+ return navigator.languages?.length
156
+ ? [...navigator.languages]
157
+ : navigator.language
158
+ ? [navigator.language]
159
+ : [];
160
+ }
161
+
162
+ function readStoredPreference(): LocalePreference | null {
163
+ if (typeof window === "undefined") return null;
164
+ try {
165
+ return normalizeLocalizationPreference(
166
+ window.localStorage.getItem(LOCALE_STORAGE_KEY),
167
+ ).locale;
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ function writeStoredPreference(preference: LocalePreference) {
174
+ if (typeof window === "undefined") return;
175
+ try {
176
+ window.localStorage.setItem(LOCALE_STORAGE_KEY, preference);
177
+ } catch {
178
+ // Ignore storage-denied browsers; the in-memory provider state still works.
179
+ }
180
+ }
181
+
182
+ function readHydrationPayload(): LocaleHydrationPayload {
183
+ if (typeof window === "undefined") return {};
184
+ return window[LOCALE_HYDRATION_GLOBAL] ?? {};
185
+ }
186
+
187
+ function resolveInitialState(args: {
188
+ initialLocale?: LocaleCode;
189
+ initialPreference?: LocalizationPreference | LocalePreference;
190
+ }): { locale: LocaleCode; preference: LocalePreference } {
191
+ const hydration = readHydrationPayload();
192
+ const preference = normalizeLocalizationPreference(
193
+ args.initialPreference ?? hydration.preference ?? readStoredPreference(),
194
+ ).locale;
195
+ const locale =
196
+ args.initialLocale ??
197
+ hydration.locale ??
198
+ resolveLocaleFromPreference(preference, browserLanguageCandidates());
199
+ return { locale, preference };
200
+ }
201
+
202
+ function normalizeLoadedMessages(value: unknown): LocaleMessages | null {
203
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
204
+ const maybeDefault = (value as { default?: unknown }).default;
205
+ if (
206
+ maybeDefault &&
207
+ typeof maybeDefault === "object" &&
208
+ !Array.isArray(maybeDefault)
209
+ ) {
210
+ return maybeDefault as LocaleMessages;
211
+ }
212
+ return value as LocaleMessages;
213
+ }
214
+
215
+ function createI18nInstance(args: {
216
+ namespace: string;
217
+ sourceLocale: LocaleCode;
218
+ messages: LocaleMessages;
219
+ initialLocale: LocaleCode;
220
+ initialMessages?: LocaleMessages | null;
221
+ }): I18nInstance {
222
+ const instance = i18next.createInstance();
223
+ const resources: Record<string, Record<string, LocaleMessages>> = {
224
+ [args.sourceLocale]: {
225
+ [args.namespace]: args.messages,
226
+ },
227
+ };
228
+ if (args.initialLocale !== args.sourceLocale && args.initialMessages) {
229
+ resources[args.initialLocale] = {
230
+ [args.namespace]: args.initialMessages,
231
+ };
232
+ }
233
+ void instance.use(initReactI18next).init({
234
+ resources,
235
+ lng: args.initialLocale,
236
+ fallbackLng: args.sourceLocale,
237
+ defaultNS: args.namespace,
238
+ ns: [args.namespace],
239
+ interpolation: { escapeValue: false },
240
+ react: { useSuspense: false },
241
+ returnNull: false,
242
+ initAsync: false,
243
+ });
244
+ return instance;
245
+ }
246
+
247
+ export function AgentNativeI18nProvider({
248
+ children,
249
+ catalog,
250
+ initialLocale,
251
+ initialPreference,
252
+ initialMessages,
253
+ persistPreference = true,
254
+ }: AgentNativeI18nProviderProps) {
255
+ const namespace = catalog?.namespace ?? "translation";
256
+ const sourceLocale = catalog?.sourceLocale ?? DEFAULT_LOCALE;
257
+ const sourceMessages = catalog?.messages ?? {};
258
+ const loadMessages = catalog?.loadMessages;
259
+ const hydration = readHydrationPayload();
260
+ const initialState = useMemo(
261
+ () => resolveInitialState({ initialLocale, initialPreference }),
262
+ [initialLocale, initialPreference],
263
+ );
264
+ const [preference, setPreferenceState] = useState<LocalePreference>(
265
+ initialState.preference,
266
+ );
267
+ const [locale, setLocale] = useState<LocaleCode>(initialState.locale);
268
+ const [loading, setLoading] = useState(false);
269
+ const i18nRef = useRef<I18nInstance | null>(null);
270
+
271
+ if (!i18nRef.current) {
272
+ const preloadedMessages = normalizeLoadedMessages(
273
+ hydration.locale === initialState.locale && hydration.messages
274
+ ? hydration.messages
275
+ : initialMessages,
276
+ );
277
+ i18nRef.current = createI18nInstance({
278
+ namespace,
279
+ sourceLocale,
280
+ messages:
281
+ hydration.locale === sourceLocale && hydration.messages
282
+ ? hydration.messages
283
+ : sourceMessages,
284
+ initialLocale: initialState.locale,
285
+ initialMessages: preloadedMessages,
286
+ });
287
+ }
288
+
289
+ const i18n = i18nRef.current;
290
+
291
+ useEffect(() => {
292
+ if (i18n.hasResourceBundle(sourceLocale, namespace)) {
293
+ i18n.addResourceBundle(
294
+ sourceLocale,
295
+ namespace,
296
+ sourceMessages,
297
+ true,
298
+ true,
299
+ );
300
+ }
301
+ }, [i18n, namespace, sourceLocale, sourceMessages]);
302
+
303
+ useEffect(() => {
304
+ const nextLocale =
305
+ preference === "system"
306
+ ? resolveLocaleFromCandidates(browserLanguageCandidates())
307
+ : preference;
308
+ setLocale(nextLocale);
309
+ }, [preference]);
310
+
311
+ useEffect(() => {
312
+ let cancelled = false;
313
+
314
+ async function applyLocale() {
315
+ setLoading(true);
316
+ try {
317
+ if (
318
+ locale !== sourceLocale &&
319
+ !i18n.hasResourceBundle(locale, namespace)
320
+ ) {
321
+ const preloaded =
322
+ hydration.locale === locale && hydration.messages
323
+ ? hydration.messages
324
+ : initialLocale === locale && initialMessages
325
+ ? initialMessages
326
+ : null;
327
+ const loaded = preloaded ?? (await loadMessages?.(locale));
328
+ const messages = normalizeLoadedMessages(loaded);
329
+ if (messages) {
330
+ i18n.addResourceBundle(locale, namespace, messages, true, true);
331
+ }
332
+ }
333
+ if (!cancelled) {
334
+ await i18n.changeLanguage(locale);
335
+ }
336
+ } finally {
337
+ if (!cancelled) setLoading(false);
338
+ }
339
+ }
340
+
341
+ void applyLocale();
342
+ return () => {
343
+ cancelled = true;
344
+ };
345
+ }, [
346
+ hydration.locale,
347
+ hydration.messages,
348
+ i18n,
349
+ initialLocale,
350
+ initialMessages,
351
+ loadMessages,
352
+ locale,
353
+ namespace,
354
+ sourceLocale,
355
+ ]);
356
+
357
+ useEffect(() => {
358
+ if (typeof document === "undefined") return;
359
+ const dir = localeDirection(locale);
360
+ const root = document.documentElement;
361
+ root.setAttribute("lang", locale);
362
+ root.setAttribute("dir", dir);
363
+ root.setAttribute("data-locale", locale);
364
+ }, [locale]);
365
+
366
+ useEffect(() => {
367
+ if (!persistPreference) return;
368
+ let cancelled = false;
369
+ callAction<LocalizationPreference>(
370
+ "get-localization-preference",
371
+ undefined,
372
+ { method: "GET" },
373
+ )
374
+ .then((value) => {
375
+ if (cancelled) return;
376
+ const normalized = normalizeLocalizationPreference(value).locale;
377
+ setPreferenceState(normalized);
378
+ writeStoredPreference(normalized);
379
+ })
380
+ .catch(() => {
381
+ // Anonymous/public routes use localStorage/browser language only.
382
+ });
383
+ return () => {
384
+ cancelled = true;
385
+ };
386
+ }, [persistPreference]);
387
+
388
+ useEffect(() => {
389
+ if (typeof window === "undefined") return;
390
+ const onStorage = (event: StorageEvent) => {
391
+ if (event.key !== LOCALE_STORAGE_KEY || event.newValue == null) return;
392
+ setPreferenceState(
393
+ normalizeLocalizationPreference(event.newValue).locale,
394
+ );
395
+ };
396
+ window.addEventListener("storage", onStorage);
397
+ return () => window.removeEventListener("storage", onStorage);
398
+ }, []);
399
+
400
+ useEffect(() => {
401
+ void setClientAppState(
402
+ "localization",
403
+ {
404
+ locale,
405
+ preference,
406
+ dir: localeDirection(locale),
407
+ },
408
+ { requestSource: "localization" },
409
+ ).catch(() => {
410
+ // Public/anonymous pages cannot write app-state; localization still works.
411
+ });
412
+ }, [locale, preference]);
413
+
414
+ const setPreference = useCallback(
415
+ async (next: LocalePreference) => {
416
+ const normalized = normalizeLocalizationPreference(next).locale;
417
+ setPreferenceState(normalized);
418
+ writeStoredPreference(normalized);
419
+ if (!persistPreference) return;
420
+ try {
421
+ await callAction<LocalizationPreference>(
422
+ "set-localization-preference",
423
+ {
424
+ locale: normalized,
425
+ },
426
+ );
427
+ } catch (error) {
428
+ const status = (error as { status?: unknown })?.status;
429
+ if (status !== 401 && status !== 403) {
430
+ throw error;
431
+ }
432
+ }
433
+ },
434
+ [persistPreference],
435
+ );
436
+
437
+ const context = useMemo<LocaleContextValue>(
438
+ () => ({
439
+ locale,
440
+ preference,
441
+ dir: localeDirection(locale),
442
+ metadata: LOCALE_METADATA[locale],
443
+ setPreference,
444
+ loading,
445
+ }),
446
+ [loading, locale, preference, setPreference],
447
+ );
448
+
449
+ return (
450
+ <LocaleContext.Provider value={context}>
451
+ <I18nextProvider i18n={i18n}>{children}</I18nextProvider>
452
+ </LocaleContext.Provider>
453
+ );
454
+ }
455
+
456
+ export function useLocale(): LocaleContextValue {
457
+ const value = useContext(LocaleContext);
458
+ if (!value) {
459
+ throw new Error("useLocale must be used within AgentNativeI18nProvider");
460
+ }
461
+ return value;
462
+ }
463
+
464
+ export function useT() {
465
+ return useTranslation().t;
466
+ }
467
+
468
+ export function useFormatters() {
469
+ const { locale } = useLocale();
470
+ return useMemo(
471
+ () => ({
472
+ formatDate(
473
+ value: Date | number | string,
474
+ options?: Intl.DateTimeFormatOptions,
475
+ ) {
476
+ return new Intl.DateTimeFormat(locale, options).format(
477
+ value instanceof Date ? value : new Date(value),
478
+ );
479
+ },
480
+ formatNumber(value: number, options?: Intl.NumberFormatOptions) {
481
+ return new Intl.NumberFormat(locale, options).format(value);
482
+ },
483
+ formatRelativeTime(
484
+ value: number,
485
+ unit: Intl.RelativeTimeFormatUnit,
486
+ options?: Intl.RelativeTimeFormatOptions,
487
+ ) {
488
+ return new Intl.RelativeTimeFormat(locale, options).format(value, unit);
489
+ },
490
+ formatList(value: string[], options?: Intl.ListFormatOptions) {
491
+ return new Intl.ListFormat(locale, options).format(value);
492
+ },
493
+ }),
494
+ [locale],
495
+ );
496
+ }
497
+
498
+ export function LanguagePicker({
499
+ className,
500
+ includeSystem = true,
501
+ label,
502
+ variant = "select",
503
+ }: {
504
+ className?: string;
505
+ includeSystem?: boolean;
506
+ label?: string;
507
+ variant?: "select" | "icon";
508
+ }) {
509
+ const { locale, preference, setPreference } = useLocale();
510
+ const copy =
511
+ LANGUAGE_PICKER_COPY[locale] ?? LANGUAGE_PICKER_COPY[DEFAULT_LOCALE];
512
+ const resolvedLabel = label ?? copy.label;
513
+ const options = [
514
+ ...(includeSystem
515
+ ? [
516
+ {
517
+ value: "system" as const,
518
+ label: copy.system,
519
+ description: copy.systemDescription,
520
+ },
521
+ ]
522
+ : []),
523
+ ...SUPPORTED_LOCALES.map((code) => ({
524
+ value: code,
525
+ label:
526
+ LOCALE_METADATA[code].nativeName === LOCALE_METADATA[code].englishName
527
+ ? LOCALE_METADATA[code].nativeName
528
+ : `${LOCALE_METADATA[code].nativeName} (${LOCALE_METADATA[code].englishName})`,
529
+ description: code,
530
+ })),
531
+ ];
532
+ const selected = options.find((option) => option.value === preference);
533
+
534
+ return (
535
+ <div className={className}>
536
+ <SelectPrimitive.Root
537
+ value={preference}
538
+ onValueChange={(value) =>
539
+ void setPreference(normalizeLocalizationPreference(value).locale)
540
+ }
541
+ >
542
+ <SelectPrimitive.Trigger
543
+ className={
544
+ variant === "icon"
545
+ ? "flex h-8 w-8 items-center justify-center rounded-md border border-border bg-background text-foreground outline-none transition-colors hover:bg-accent/40 data-[placeholder]:text-muted-foreground"
546
+ : "flex h-9 w-full items-center justify-between rounded-md border border-border bg-background px-3 text-left text-[12px] text-foreground outline-none transition-colors hover:bg-accent/40 data-[placeholder]:text-muted-foreground"
547
+ }
548
+ aria-label={resolvedLabel}
549
+ title={selected?.label ?? resolvedLabel}
550
+ >
551
+ <span className="flex min-w-0 items-center gap-2">
552
+ <IconLanguage className="h-4 w-4 shrink-0 text-muted-foreground" />
553
+ {variant === "select" ? (
554
+ <SelectPrimitive.Value>
555
+ <span className="truncate">
556
+ {selected?.label ?? preference}
557
+ </span>
558
+ </SelectPrimitive.Value>
559
+ ) : null}
560
+ </span>
561
+ {variant === "select" ? (
562
+ <SelectPrimitive.Icon asChild>
563
+ <IconChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
564
+ </SelectPrimitive.Icon>
565
+ ) : null}
566
+ </SelectPrimitive.Trigger>
567
+ <SelectPrimitive.Portal>
568
+ <SelectPrimitive.Content
569
+ position="popper"
570
+ sideOffset={6}
571
+ className={
572
+ variant === "icon"
573
+ ? "z-[9999] min-w-56 overflow-hidden rounded-lg border border-border bg-popover shadow-lg"
574
+ : "z-[9999] w-[var(--radix-select-trigger-width)] overflow-hidden rounded-lg border border-border bg-popover shadow-lg"
575
+ }
576
+ >
577
+ <SelectPrimitive.Viewport className="p-1">
578
+ {options.map((option) => (
579
+ <SelectPrimitive.Item
580
+ key={option.value}
581
+ value={option.value}
582
+ className="relative flex w-full cursor-pointer select-none items-start gap-2 rounded-md px-8 py-2.5 text-[12px] outline-none data-[highlighted]:bg-accent/60 data-[state=checked]:bg-accent/40"
583
+ >
584
+ <span className="absolute left-2 top-2.5 flex h-4 w-4 items-center justify-center text-muted-foreground">
585
+ <SelectPrimitive.ItemIndicator>
586
+ <IconCheck className="h-3.5 w-3.5" />
587
+ </SelectPrimitive.ItemIndicator>
588
+ </span>
589
+ <div className="flex min-w-0 flex-col">
590
+ <SelectPrimitive.ItemText>
591
+ <span className="text-foreground">{option.label}</span>
592
+ </SelectPrimitive.ItemText>
593
+ <span className="mt-0.5 text-[11px] leading-relaxed text-muted-foreground">
594
+ {option.description}
595
+ </span>
596
+ </div>
597
+ </SelectPrimitive.Item>
598
+ ))}
599
+ </SelectPrimitive.Viewport>
600
+ </SelectPrimitive.Content>
601
+ </SelectPrimitive.Portal>
602
+ </SelectPrimitive.Root>
603
+ </div>
604
+ );
605
+ }