3å¹Žéæ¥æ¬èªã®ã¿ã§éçšããŠãããµãŒãã¹ãã3ã¶æã§æ°äººãå€èšèªåãã話
ã¯ãããŸããŠãFiNCã§ä»å¹Žã®4æãããµãŒããŒãµã€ããšã³ãžãã¢ããã£ãŠããæŸ€äºã§ãã
å
æ¥ãã¬ã¹ãªãªãŒã¹ããããŸãããšãããFiNCã§æäŸããŠããæ³äººåããµãŒãã¹FiNCãã©ã¹ãæ¥æ¬èªä»¥å€ã«ã察å¿ãããã®åºŠè±èªçããªãªãŒã¹ãããŸããã
å
¥ç€ŸçŽåŸã«æºãã£ãæåã®ãããžã§ã¯ããšããããšã§ãå人çã«ã¯éåžžã«ææ
šæ·±ãã®ã§ãããä»åã¯ãã®ãããžã§ã¯ãã®èå°è£ãèŠãŠãããããšæããŸãã
ãµãŒãã¹ã®å€èšèªå¯Ÿå¿ãšèšã£ãŠãããã®ã¹ã³ãŒãã¯æ§ã
ã§ããããµãŒãã¹ãæäŸããŠããããŒã¿ã¯å€å²ã«ããããŸãã ãã£ããã«ããŽã©ã€ãºããã ãã§ãã
ã¿ã€ã ã©ã€ã³ãªã©ãŠãŒã¶ãŒæçš¿ã«ããããŒã¿
ããããã®ã¿ã¹ã¯ãªã©ã®ãã¹ã¿ãŒããŒã¿
ããããããããããã¹ãã®ãã®ãšç»åããŒã¿ããããŸãã
ãŸããããã«çŽä»ããŠã
ãŠãŒã¶ãŒäœ¿çšèšèªã®å€å®
ãŠãŒã¶ãŒãããã¹ããããããŒã¿ã翻蚳ããããšã¯ããã¡ããã¯å¯èœãªã®ã§ããããŠãŒã¶ãŒã®æå³ãæãªãå¯èœæ§ãé«ãã®ã§ãäœãããªããšããæ¹éãæ¡çšãããã以å€ã®UIããã³æäŸããŠãããã¹ã¿ãŒããŒã¿ã«ã€ããŠã¯å€èšèªå¯Ÿå¿ããããšã§ãå€èšèªç°å¢äžã§ååãªUXãæäŸãããšããããšã«ãªããŸããã
ãã€ã¯ããµãŒãã¹ã«ãããå€èšèªå
FiNCã®ãµãŒãã¹ã¯ãã€ã¯ããµãŒãã¹ãšãã圢ã§éçšãããŠããŸãã ãµãŒããŒã¯åºæ¬çã«Ruby on Railsã§éçšãããŠããŠãã¯ã©ã€ã¢ã³ããiOSãAndroidããã³Webãã©ãŠã¶(React.js)ã§ããããã¢ããªãæäŸãããŠããŸãã
åºæ¬å€èšèªåæ¹éïŒãµãŒããŒãµã€ãïŒ
ãµãŒããŒãµã€ãã§å€èšèªå察å¿ãããã®ã¯äž»ã«äžèšã®3ã€ã§ãã
ã³ãŒãå
ã®æ¥æ¬èª (Web Viewãªã©ïŒ
1.ãœãŒã¹ã³ãŒãå
ã®æ¥æ¬èª
Railsã¯ããã©ã«ãã§i18n APIãæäŸããŠããŸããWeb Viewãã¯ãããã³ãŒãå
ã«å®è£
ãããŠããæ¥æ¬èªã«ã€ããŠã¯ãå
šãŠi18n APIã䜿çšããããšã«ããŸããã ãã ããi18n APIã¯1ç¹ã ãé£ããç¹ããããŸããããã¯ãèŠçŽãéèŠããRailsã«ãããŠããã®éšåã ãã¯ã»ãšãã©èŠçŽããªããšããç¹ã§ããããããã«ã§ããã¡ã€ã«ãé
眮ã§ããŠããŸããŸããViewã§ã¯Lazy Lookupãšããæ©èœãæäŸãããŠããããã¡ã€ã«ã®ãã£ã¬ã¯ããªæ§æãšç¿»èš³ãã¡ã€ã«ã®éå±€æ§é ãããããããšã§ãã¥ãŒå
éšã«é·ãéå±€ãæžããªããŠãåç
§ã§ããããã«ãªã£ãŠããŸãã
Railsã¬ã€ãã®äŸãã®ãŸãŸã§ããã
en: movies: index: title: "Frozen"
ãšããèŸæžãã¡ã€ã«ãyaml圢åŒã§çšæããŠãããapp/views/movies/index.html.erbå
ã§ã以äžã®ããã«æžãã ãã§movies.index.titleãåç
§ã§ããŸãã
ãšãªããŸãã ãã®åœ¢åŒãåèã«ããŠç¿»èš³ãã¡ã€ã«é
çœ®ã®æ§æã¯ãconfig/locale以äžã«ã¢ããªã±ãŒã·ã§ã³ã³ãŒããšåããã¡ã€ã«éå±€ãäœæããããšã«ããŸããã
app ââapi â ââv1 â ââusers_api.rb config ââlocales ââapi ââv1 ââusers ââja.yml ââen.yml
en: api: v1: users: name: 'Name'
DBå
ã«ä¿åãããŠãããã¹ã¿ãŒããŒã¿ãè±èªåããã«ã¯ãæ¥æ¬èªãšå¯Ÿå¿ããããŒã¿ãDBå
ã®ã©ããã«ä¿åããªããã°ãªããŸããã ã±ã£ãšè°è«ããŠåºãŠããæ¹éã¯ã以äžã®2ã€ã§ããã
åããŒãã«å
ã§æ¥æ¬èªã®é£ã«è±èªçšã®ã«ã©ã ãäœæããã
å€èšèªå¯Ÿå¿çšã«å¥ããŒãã«ãäœæããã
å°ãèããã°ããããŸãããå°æ¥èšèªã远å ããå Žåã1ã®æ¹éã§ã¯ã«ã©ã ã1ã€ã〠å¢ããŠãããšããã¢ã³ããã¿ãŒã³ãçºçããŸãã ãã®ãããä»åã¯2ã®å¥ããŒãã«ãäœæããæ¹éãæ¡çšããŸãããRailsã§ã¯DBã®å€èšèªå¯Ÿå¿çšã«globalizeãšããæåãªgemïŒRubyã©ã€ãã©ãªïŒãçšæãããŠããã®ã§ãã¡ããæ¡çšããŸããã
mysql> select * from jobs; +----+--------------+ | id | name | +----+--------------+ | 1 | ã¢ã¹ãªãŒã | | 2 | å°éå®¶ | +----+--------------+ 2 rows in set (0.00 sec)
mysql> select * from job_translations; +----+--------+--------+---------------------+ | id | job_id | locale | name | +----+--------+--------+---------------------+ | 1 | 1 | en | Athlete | | 2 | 2 | en | Specialist | +----+--------+--------+---------------------+ 2 rows in set (0.01 sec)
ãã ãããã®æ¹éã¯ç¿»èš³å¯Ÿè±¡ã®ããŒãã«ãèªåçã«associationã®ããŒãã«ãæ°ãã«æã€ããšã«ãªããŸãã®ã§ããªã«ãããªãã§ãããšå€§éã®N+1ã¯ãšãªãçºçããŸãã
äŸãã°äžã®äŸã ãšã翻蚳ããŒãã«ããªãã®å Žåãæ¬¡ã®ãããªã¯ãšãªãèµ°ããŸãã
[1] pry(main)> Job.all Job Load (0.3ms) SELECT `jobs`.* FROM `jobs`
ãããã翻蚳ããŒãã«ãäœæãasscociteãããŠãããšä»¥äžã®ãããªã¯ãšãªãèµ°ããŸããäžè¡ã§æžãã§ããã¯ãšãªãããŒãã«ã®è¡æ°åèµ°ã£ãŠããã®ã§ãã
[1] pry(main)> Job.all Job Load (2.9ms) SELECT `jobs`.* FROM `jobs` Job::Translation Load (0.5ms) SELECT `job_translations`.* FROM `job_translations` WHERE `job_translations`.`job_id` = 1 Job::Translation Load (0.5ms) SELECT `job_translations`.* FROM `job_translations` WHERE `job_translations`.`job_id` = 2
ããã解決ããã«ã¯railsãæäŸããŠããeager loadingã®æ©èœã䜿ãããšã§å®çŸã§ããŸãã
[1] pry(main)> Job.includes(:translations).all Job Load (0.5ms) SELECT `jobs`.* FROM `jobs` Job::Translation Load (0.4ms) SELECT `job_translations`.* FROM `job_translations` WHERE `job_translations`.`job_id` IN (1, 2)
ã³ãŒãå
ã«includesã远å ããŠããã«ã¯2çš®é¡ã®æ¹æ³ããããŸãã1ã€ã¯default scopeãšããrailsãæäŸããŠããæ©èœãããäžã€ã¯åå¥ã«å¿
èŠãªç®æã«includesã远å ããŠããæ¹æ³ã§ãã
defaut scopeã䜿ããšã1è¡ã§ç°¡åã«å
šãŠã®ã¯ãšãªã§ããã©ã«ãã§çºè¡ãããæ€çŽ¢æ¡ä»¶ãèšå®ããããšãã§ããŸããäŸãã°äžã®äŸã ãš
class Job < ActiveRecord::Base default_scope -> { includes(:translations) } end
ã®ããã«èšå®ã§ããŸãããã ãããã®æ¹æ³ã§ã¯includesã§ã¯ãªãpreloadãeager_loadãªã©ã®å¥ã®ã¡ãœããã§ããŒãã«ã®Eager loadingã®æ©èœã䜿ããããšãã«ããå¿
ãdefault scopeã§èšå®ããã¯ãšãªãäœèšã«èµ°ã£ãŠããŸããšããåé¡ããããŸãã
ä»åã¯äžèšã®ãããªã¡ãªããã»ãã¡ãªãããæ¯èŒããçµæãdefault scopeã¯çšããã«ç¿»èš³ããŒãã«ãåŒãã§ããç®æã«includesã远å ããŠããããšã«ããŸãããïŒ20åã»ã©ç¿»èš³ããŒãã«ããããŸããããå
šéšã§70ç®æãããä¿®æ£ããŸãããïŒ
å°ãªãã®ã§ãããã¥ãŒããªã¢ã«ãªã©çšã«ç»åã§æäŸããŠãããã¹ã¿ãŒããŒã¿ããããŸãã çŸåšç»åã®ããŒã¿ã«ã€ããŠã¯ã2çš®é¡ã®æ¹æ³ã§ç®¡çãããŠããŸãã
å€éšã¹ãã¬ãŒãžã«ä¿åãããã®URLãDBã«ä¿æããã
ãœãŒã¹ã³ãŒããšåããªããžããªã«é
眮ããRailsã®asset管çã®ä»çµã¿ã䜿ã£ãŠåŒã³åºãã
1ã«ã€ããŠã¯ããã¹ã¿ãŒããŒã¿ã®äŸãšåããåèšèªã«å¯Ÿå¿ããç»åãå€éšã¹ãã¬ãŒãžã«ä¿åããããããã®URLãglobalizeã®gemãçšããŠåºãåããããšã§å¯Ÿå¿ããŸããã 2ã«ã€ããŠã¯ãã¢ããªã±ãŒã·ã§ã³ã®ã³ãŒãå
ã§å€èšèªåãå¿
èŠãªç»åã®ãã¹ã®ã«ãŒã«ãèšå®ããlocaleæ
å ±ãåç
§ããŠç»åã®èªã¿èŸŒã¿å
ã倿Žããããšã§å¯Ÿå¿ããŸããã
app/assets/images/apis/v1/onboarding ââ en â ââ recommended.png ââ ja ââ recommended.png
def contents { image_url: ActionController::Base.helpers.image_url("apis/v1/onboarding/#{I18n.locale}/recommended.png") } end
åºæ¬å€èšèªåæ¹éïŒã¯ã©ã€ã¢ã³ããµã€ãïŒ
âšã¯ã©ã€ã¢ã³ãã®ç¿»èš³ã¯UIéšåã«é¢ãããã®ã ãã§ãã
èšèªèšå®ã®æ¹æ³ã¯ã
端æ«ã®èšèªèšå®ã䜿çš
ã¢ããªåŽã§å¥ã«èšå®ã§ããããã«ãã
ã®2çš®é¡ãããããã®ã§ãããä»åã¯äžçªç°¡åãªç«¯æ«èšèªæ
å ±ãå©çšããæ¹éãæ¡çšããŸããã
ãŠãŒã¶ãŒã®äœ¿çšèšèªãã©ã管çããããšããã®ã¯ãã€ã¯ããµãŒãã¹ã§éçšããŠããFiNCã«ãšã£ãŠéåžžã«éèŠãªåé¡ã§ãã åãµãŒãã¹éã®æŽåæ§ãä¿ã¡ãŠãŒã¶ã«çµ±äžããäœéšãäžããå¿
èŠãæããããåãµãŒãã¹ã¯ãŠãŒã¶ãŒã®äœ¿çšèšèªãäœããã®æ¹æ³ã§ãã¡ããšç¥ã£ãŠããå¿
èŠããããŸãããããåãµãŒãã¹ããäžçšæã«ãŠãŒã¶ãŒã®äœ¿çšèšèªã倿ŽããŠããŸããšãããããªã£ãŠããŸããŸãã è€æ°ããã€ã¹ãæã£ãŠãããŠãŒã¶ãŒã®æ³å®ãªã©ãè²ã
ãšè°è«ããçµæäžèšã®æ¹éãæ¡çšããŸããã
èšèªèšå®ã¯åºæ¬çã«ç«¯æ«ã®äœ¿çšèšèªãæ£ãšãã
䜿çšèšèªèšå®ã®èšå®ã倿Žã§ããã®ã¯ç«¯æ«ã®ã¯ã©ã€ã¢ã³ãã®ã¿
ãŠãŒã¶ãŒãçŸåšç«¯æ«ã§èšå®ããŠããèšèªæ
å ±ã¯ãŠãŒã¶ãŒæ
å ±ã®ãµãŒãã¹å
ã§ãä¿åããŠãã ïŒäœ¿çšèšèªèšå®ã倿Žã§ããAPIããã£ãŠãŠãŒã¶ãŒã䜿çšèšèªã倿ŽããåŸã¢ããªãéããéã«æŽæ°çšã®APIãå©ãããïŒ
åãµãŒãã¹ã¯äœ¿çšèšèªãäžèš2çš®é¡ã®ããããã®æ¹æ³ã§ååŸãã
端æ«ã¯APIããã³web_viewã«ã¢ã¯ã»ã¹ããéã«äœ¿çšèšèªæ
å ±ããããã§éãã
åãµãŒãã¹ã¯ããããã¿ãŠèšèªæ
å ±ãååŸããããããããªããã°ãŠãŒã¶ãŒæ
å ±ããã¡ã€ã³äœ¿çšèšèªæ
å ±ãååŸããã
ã©ã¡ããèŠã€ãããªããããã©ã«ãã§æ¥æ¬èªãååŸããã
ãã©ãŠã¶ããŒã¹ã®ãµãŒãã¹ã¯ãã©ãŠã¶ã®äœ¿çšèšèªããååŸããã
éç¥ãªã©äœ¿ã£ãŠããŠãŒã¶ãŒã§ã¯ãªããé£ã³å
ã®ãŠãŒã¶ãŒã®äœ¿çšèšèªãå¿
èŠãªç©ã¯ãéä¿¡å
ã®ãŠãŒã¶ãŒæ
å ±ãããŒãã«ããååŸããã
ãŠãŒã¶ãŒæ
å ±ç®¡çããã€ã¯ããµãŒãã¹ãšããŠåãåºãããŠããã®ã§ãããåãµãŒãã¹ããŠãŒã¶èªèšŒãµãŒãã¹ã«ã¢ã¯ã»ã¹ããŠäœ¿çšèšèªãååŸããéšåã¯ãå
šãµãŒãã¹å
±éã§å¿
èŠãªåŠçãªã®ã§ã瀟å
gemãšããŠäœæã»é
åžãè¡ããŸããã
å€èšèªåã«ã€ããŠã¯ãæè¡çãªæ¹ä¿®ä»¥å€ã«æèšã®ç¿»èš³äŸé Œãšç¶ç¶çãªãã©ãã·ã¥ã¢ãããå¿
èŠã§ãããã®ããã«ã¯ãšã³ãžãã¢ã ãã§ã¯ãªããProduct DevelopmentãšåŒã°ããProject Mangement(PM)ã¬ã€ã€ãŒã®ããŒã ããã®çŽæ¥ã®ã³ã³ããªãã¥ãŒããéèŠã«ãªã£ãŠããŸããä»åå€èšèªå¯Ÿå¿ã§æ¡çšããããããåãçµã¿ã«ã€ããŠä»¥äžã«ç޹ä»ããŸãã
Gengo webapp ä»åã®ç¿»èš³ã§ã¯æ ªåŒäŒç€ŸGengoã®ç¿»èš³ãµãŒãã¹ã䜿çšããŸãããGengo瀟ã®ç¿»èš³ãµãŒãã¹ã¯ããŠã§ããµã€ãçµç±ã§æ³šæããã®ãšãAPIçµç±ã§æ³šæãããã®ãš2çš®é¡ã®æ¹æ³ãçšæãããŠããã®ã§ãããAPIã®æ¹ã15%ã»ã©äŸ¡æ Œãå®ãèšå®ãããŠããŸããä»åã¯APIçµç±ã®çºæ³šã«ããã®ã§ãããã³ãã³ãããå©ãã®ã§ã¯ãšã³ãžãã¢ããçºæ³šã§ããªããªã£ãŠããŸããŸãããã®ããä»åã¯Gengoçºæ³šçšã«ç€Ÿå
ããŒã«ãäœæããGUIãçšããŠAPIçµç±ã§çºæ³šã§ããããã«ããŸããã瀟å
çã«ãéå»ã®ç¿»èš³äºäŸãDBã«ä¿åã§ããã®ã§éåžžã«å®å¿ã§ãããPMã¬ã€ã€ãŒã®äººéãçºæ³šãè¡ããããã«ãªã£ãŠéåžžã«äŸ¿å©ã«ãªããŸããã
XLIFFã«ããéçš ã¯ã©ã€ã¢ã³ããµã€ãã«ã€ããŠã¯XLIFFãšãããã¡ã€ã«ãã©ãŒãããã§éçšãè¡ãããšã«ããŸããã XLIFF(XML Localization Interchange File Format)ã¯Xcode6ãããµããŒãããã翻蚳ã®ããã®æšæºèŠæ Œã§ãAndroid Studioã§ã䜿çšå¯èœã§ããFiNCã§ã¯ã¯ã©ã€ã¢ã³ãã®æèšç®¡ççšã«XLIFFãã¡ã€ã«åäœãgit管çããŠããŸããXLIFFèªäœã¯ãšã³ãžãã¢ã ãã§ãªãProduct DevelopmentããŒã ãPRãã ããŠæŽæ°ããŠãã圢ã«ããŠããããªãªãŒã¹ããã®ã¡ãå®åžžçã«äžèªç¶ãªè¡šçŸã®ä¿®æ£ãªã©ãè¡ãããŠããŸãããŸããXLIFF1ã€ã§iOSãAndroidãäž¡æ¹ã管çããããšã«ãããããã€ã¹ãåããã¯ã©ã€ã¢ã³ãã®æèšãåäžã«ããããšãã§ããiOSãAndroidã§å
±éã®ãŠãŒã¶ãŒäœéšãæäŸã§ããããã«ãªããŸããã
Server å€èšèªå¯Ÿå¿ããªãªãŒã¹ããããšã«ãããæ¥æ¬èªæååãããŒãã³ãŒããããããšã¯ãã»ãšãã©ãªããªããŸãããéçºè
ã®å¢ããŠããŠããFiNCã§ã¯ããã§ã誰ããæ¥æ¬èªããŒãã³ãŒããæ··å
¥ãããŠããŸãå¯èœæ§ã¯åžžã«ãããŸããéçšã®ä»çµã¿çã«ãããæé€ãããããCIæã«æ¥æ¬èªã®ããŒãã³ãŒãããããããã§ãã¯ãããã¹ãã远å ãããã¹ãã¬ãã«ããŒãã³ãŒãã®æ··å
¥ããããä»çµã¿ãå°å
¥ããŸããã
require 'find' # i18nã䜿ããã«slimãã³ãã¬ãŒããäœã£ãå Žåã«æ€ç¥ãããã¹ã # ã³ã¡ã³ãã¯èš±å®¹ããŠãã # æ°èŠã«äœã£ããã³ãã¬ãŒãã§ãã®ãã¹ãã倱æããå Žå㯠# 察象ã®ãã³ãã¬ãŒãã§i18nã䜿ãããã«ä¿®æ£ããŠãã ãã # æ¢åãã䜿ã£ãŠãããã®ã¯blacklistã«å®çŸ©ããŠãããã # æ°èŠã«äœã£ããã³ãã¬ãŒããblacklistã«è¿œå ããã®ã¯NG blacklist = %w( app/views/index.html.slim ... ... ... ) blacklist_map = {} blacklist.each do |file| blacklist_map[file] = true end describe 'i18n view' do Find.find('app/views') do |file| next if file.match(/\Aapp\/views\/admins/) next unless file.match(/\.slim\z/) next if blacklist_map[file] context "when #{file} is found" do it 'the file does not have multibyte string' do expect(has_multibyte_string(file)).to be false end end end private def has_multibyte_string(file) File.foreach(file) do |line| if is_multibyte_line(line) return true end end false end def is_multibyte_line(line) # ã³ã¡ã³ãã¯ã¹ã«ãŒ if line.match(%r{\A\s*/}) return false end # æ¥æ¬èªãå«ãã§ããã return line.match(/[ã-ãã¡-ãŽäž-éŸ ]/u) end end
ããŸãïŒOSSãžã®è²¢ç®ïŒ
ä»åglobalizeã®gemãå°å
¥ããã®ã§ããã䜿çšããäžã§globalizeã®assign_attributesã¡ãœããã«åŒæ°ãç Žå£ãããã°ãæãããšãããããŸãããFiNCã§ã¯ç©æ¥µçã«OSSãžã®è²¢ç®ãæèããŠããããã®ãã°ã«å¯ŸããŠãPRãéãããŒãžãããŸããã
FiNCã§ã¯å
¥ç€Ÿããã«ã§ããã®ãããªè²¬ä»»ãã倧ããªä»äºãä»»ããŠãããããšãã§ããŸããããããžã§ã¯ããéããŠæé·ããŠãããšãã§ããŸããïŒFiNCã§ã¯ãå
šãŠã®ãããã¯ãã«èªããæã£ãŠããã®ã§ãæ°äººã®ä»äºã§ã培åºçã«ã¬ãã¥ãŒããããã©ãã·ã¥ã¢ãããããŸããããããããã»ã¹ãçµãŠæ°äººã§ãæé·ããŠããããšãã§ããŸããïŒ
FiNCã§ã¯ç¶ç¶çã«ãšã³ãžãã¢ãåéããŠããŸãïŒ
èå³ã®ããæ¹ã¯ãæ°è»œã«ãé£çµ¡ãã ããïŒ
iOS -> https://www.wantedly.com/projects/59939
Android -> https://www.wantedly.com/projects/54470
Webããã³ã -> https://www.wantedly.com/projects/63233
ã€ã³ãã©/SRE -> https://www.wantedly.com/projects/57858
AI/æ©æ¢°åŠç¿ -> https://www.wantedly.com/projects/57462
åæ/ããŒã¿ã¢ããªã¹ã -> https://www.wantedly.com/projects/57201
QA/ãã¹ããšã³ãžã㢠-> https://www.wantedly.com/projects/64774