google_i18n 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (270) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +57 -0
  7. data/README.md +89 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/google_i18n.gemspec +45 -0
  12. data/lib/google_i18n.rb +12 -0
  13. data/lib/google_i18n/address.rb +495 -0
  14. data/lib/google_i18n/data/ac.json +1 -0
  15. data/lib/google_i18n/data/ad.json +1 -0
  16. data/lib/google_i18n/data/ae.json +1 -0
  17. data/lib/google_i18n/data/af.json +1 -0
  18. data/lib/google_i18n/data/ag.json +1 -0
  19. data/lib/google_i18n/data/ai.json +1 -0
  20. data/lib/google_i18n/data/al.json +1 -0
  21. data/lib/google_i18n/data/all.json +1 -0
  22. data/lib/google_i18n/data/am.json +1 -0
  23. data/lib/google_i18n/data/ao.json +1 -0
  24. data/lib/google_i18n/data/aq.json +1 -0
  25. data/lib/google_i18n/data/ar.json +1 -0
  26. data/lib/google_i18n/data/as.json +1 -0
  27. data/lib/google_i18n/data/at.json +1 -0
  28. data/lib/google_i18n/data/au.json +1 -0
  29. data/lib/google_i18n/data/aw.json +1 -0
  30. data/lib/google_i18n/data/ax.json +1 -0
  31. data/lib/google_i18n/data/az.json +1 -0
  32. data/lib/google_i18n/data/ba.json +1 -0
  33. data/lib/google_i18n/data/bb.json +1 -0
  34. data/lib/google_i18n/data/bd.json +1 -0
  35. data/lib/google_i18n/data/be.json +1 -0
  36. data/lib/google_i18n/data/bf.json +1 -0
  37. data/lib/google_i18n/data/bg.json +1 -0
  38. data/lib/google_i18n/data/bh.json +1 -0
  39. data/lib/google_i18n/data/bi.json +1 -0
  40. data/lib/google_i18n/data/bj.json +1 -0
  41. data/lib/google_i18n/data/bl.json +1 -0
  42. data/lib/google_i18n/data/bm.json +1 -0
  43. data/lib/google_i18n/data/bn.json +1 -0
  44. data/lib/google_i18n/data/bo.json +1 -0
  45. data/lib/google_i18n/data/bq.json +1 -0
  46. data/lib/google_i18n/data/br.json +1 -0
  47. data/lib/google_i18n/data/bs.json +1 -0
  48. data/lib/google_i18n/data/bt.json +1 -0
  49. data/lib/google_i18n/data/bv.json +1 -0
  50. data/lib/google_i18n/data/bw.json +1 -0
  51. data/lib/google_i18n/data/by.json +1 -0
  52. data/lib/google_i18n/data/bz.json +1 -0
  53. data/lib/google_i18n/data/ca.json +1 -0
  54. data/lib/google_i18n/data/cc.json +1 -0
  55. data/lib/google_i18n/data/cd.json +1 -0
  56. data/lib/google_i18n/data/cf.json +1 -0
  57. data/lib/google_i18n/data/cg.json +1 -0
  58. data/lib/google_i18n/data/ch.json +1 -0
  59. data/lib/google_i18n/data/ci.json +1 -0
  60. data/lib/google_i18n/data/ck.json +1 -0
  61. data/lib/google_i18n/data/cl.json +1 -0
  62. data/lib/google_i18n/data/cm.json +1 -0
  63. data/lib/google_i18n/data/cn.json +1 -0
  64. data/lib/google_i18n/data/co.json +1 -0
  65. data/lib/google_i18n/data/cr.json +1 -0
  66. data/lib/google_i18n/data/cu.json +1 -0
  67. data/lib/google_i18n/data/cv.json +1 -0
  68. data/lib/google_i18n/data/cw.json +1 -0
  69. data/lib/google_i18n/data/cx.json +1 -0
  70. data/lib/google_i18n/data/cy.json +1 -0
  71. data/lib/google_i18n/data/cz.json +1 -0
  72. data/lib/google_i18n/data/de.json +1 -0
  73. data/lib/google_i18n/data/dj.json +1 -0
  74. data/lib/google_i18n/data/dk.json +1 -0
  75. data/lib/google_i18n/data/dm.json +1 -0
  76. data/lib/google_i18n/data/do.json +1 -0
  77. data/lib/google_i18n/data/dz.json +1 -0
  78. data/lib/google_i18n/data/ec.json +1 -0
  79. data/lib/google_i18n/data/ee.json +1 -0
  80. data/lib/google_i18n/data/eg.json +1 -0
  81. data/lib/google_i18n/data/eh.json +1 -0
  82. data/lib/google_i18n/data/er.json +1 -0
  83. data/lib/google_i18n/data/es.json +1 -0
  84. data/lib/google_i18n/data/et.json +1 -0
  85. data/lib/google_i18n/data/fi.json +1 -0
  86. data/lib/google_i18n/data/fj.json +1 -0
  87. data/lib/google_i18n/data/fk.json +1 -0
  88. data/lib/google_i18n/data/fm.json +1 -0
  89. data/lib/google_i18n/data/fo.json +1 -0
  90. data/lib/google_i18n/data/fr.json +1 -0
  91. data/lib/google_i18n/data/ga.json +1 -0
  92. data/lib/google_i18n/data/gb.json +1 -0
  93. data/lib/google_i18n/data/gd.json +1 -0
  94. data/lib/google_i18n/data/ge.json +1 -0
  95. data/lib/google_i18n/data/gf.json +1 -0
  96. data/lib/google_i18n/data/gg.json +1 -0
  97. data/lib/google_i18n/data/gh.json +1 -0
  98. data/lib/google_i18n/data/gi.json +1 -0
  99. data/lib/google_i18n/data/gl.json +1 -0
  100. data/lib/google_i18n/data/gm.json +1 -0
  101. data/lib/google_i18n/data/gn.json +1 -0
  102. data/lib/google_i18n/data/gp.json +1 -0
  103. data/lib/google_i18n/data/gq.json +1 -0
  104. data/lib/google_i18n/data/gr.json +1 -0
  105. data/lib/google_i18n/data/gs.json +1 -0
  106. data/lib/google_i18n/data/gt.json +1 -0
  107. data/lib/google_i18n/data/gu.json +1 -0
  108. data/lib/google_i18n/data/gw.json +1 -0
  109. data/lib/google_i18n/data/gy.json +1 -0
  110. data/lib/google_i18n/data/hk.json +1 -0
  111. data/lib/google_i18n/data/hm.json +1 -0
  112. data/lib/google_i18n/data/hn.json +1 -0
  113. data/lib/google_i18n/data/hr.json +1 -0
  114. data/lib/google_i18n/data/ht.json +1 -0
  115. data/lib/google_i18n/data/hu.json +1 -0
  116. data/lib/google_i18n/data/id.json +1 -0
  117. data/lib/google_i18n/data/ie.json +1 -0
  118. data/lib/google_i18n/data/il.json +1 -0
  119. data/lib/google_i18n/data/im.json +1 -0
  120. data/lib/google_i18n/data/in.json +1 -0
  121. data/lib/google_i18n/data/io.json +1 -0
  122. data/lib/google_i18n/data/iq.json +1 -0
  123. data/lib/google_i18n/data/ir.json +1 -0
  124. data/lib/google_i18n/data/is.json +1 -0
  125. data/lib/google_i18n/data/it.json +1 -0
  126. data/lib/google_i18n/data/je.json +1 -0
  127. data/lib/google_i18n/data/jm.json +1 -0
  128. data/lib/google_i18n/data/jo.json +1 -0
  129. data/lib/google_i18n/data/jp.json +1 -0
  130. data/lib/google_i18n/data/ke.json +1 -0
  131. data/lib/google_i18n/data/kg.json +1 -0
  132. data/lib/google_i18n/data/kh.json +1 -0
  133. data/lib/google_i18n/data/ki.json +1 -0
  134. data/lib/google_i18n/data/km.json +1 -0
  135. data/lib/google_i18n/data/kn.json +1 -0
  136. data/lib/google_i18n/data/kp.json +1 -0
  137. data/lib/google_i18n/data/kr.json +1 -0
  138. data/lib/google_i18n/data/kw.json +1 -0
  139. data/lib/google_i18n/data/ky.json +1 -0
  140. data/lib/google_i18n/data/kz.json +1 -0
  141. data/lib/google_i18n/data/la.json +1 -0
  142. data/lib/google_i18n/data/lb.json +1 -0
  143. data/lib/google_i18n/data/lc.json +1 -0
  144. data/lib/google_i18n/data/li.json +1 -0
  145. data/lib/google_i18n/data/lk.json +1 -0
  146. data/lib/google_i18n/data/lr.json +1 -0
  147. data/lib/google_i18n/data/ls.json +1 -0
  148. data/lib/google_i18n/data/lt.json +1 -0
  149. data/lib/google_i18n/data/lu.json +1 -0
  150. data/lib/google_i18n/data/lv.json +1 -0
  151. data/lib/google_i18n/data/ly.json +1 -0
  152. data/lib/google_i18n/data/ma.json +1 -0
  153. data/lib/google_i18n/data/mc.json +1 -0
  154. data/lib/google_i18n/data/md.json +1 -0
  155. data/lib/google_i18n/data/me.json +1 -0
  156. data/lib/google_i18n/data/mf.json +1 -0
  157. data/lib/google_i18n/data/mg.json +1 -0
  158. data/lib/google_i18n/data/mh.json +1 -0
  159. data/lib/google_i18n/data/mk.json +1 -0
  160. data/lib/google_i18n/data/ml.json +1 -0
  161. data/lib/google_i18n/data/mm.json +1 -0
  162. data/lib/google_i18n/data/mn.json +1 -0
  163. data/lib/google_i18n/data/mo.json +1 -0
  164. data/lib/google_i18n/data/mp.json +1 -0
  165. data/lib/google_i18n/data/mq.json +1 -0
  166. data/lib/google_i18n/data/mr.json +1 -0
  167. data/lib/google_i18n/data/ms.json +1 -0
  168. data/lib/google_i18n/data/mt.json +1 -0
  169. data/lib/google_i18n/data/mu.json +1 -0
  170. data/lib/google_i18n/data/mv.json +1 -0
  171. data/lib/google_i18n/data/mw.json +1 -0
  172. data/lib/google_i18n/data/mx.json +1 -0
  173. data/lib/google_i18n/data/my.json +1 -0
  174. data/lib/google_i18n/data/mz.json +1 -0
  175. data/lib/google_i18n/data/na.json +1 -0
  176. data/lib/google_i18n/data/nc.json +1 -0
  177. data/lib/google_i18n/data/ne.json +1 -0
  178. data/lib/google_i18n/data/nf.json +1 -0
  179. data/lib/google_i18n/data/ng.json +1 -0
  180. data/lib/google_i18n/data/ni.json +1 -0
  181. data/lib/google_i18n/data/nl.json +1 -0
  182. data/lib/google_i18n/data/no.json +1 -0
  183. data/lib/google_i18n/data/np.json +1 -0
  184. data/lib/google_i18n/data/nr.json +1 -0
  185. data/lib/google_i18n/data/nu.json +1 -0
  186. data/lib/google_i18n/data/nz.json +1 -0
  187. data/lib/google_i18n/data/om.json +1 -0
  188. data/lib/google_i18n/data/pa.json +1 -0
  189. data/lib/google_i18n/data/pe.json +1 -0
  190. data/lib/google_i18n/data/pf.json +1 -0
  191. data/lib/google_i18n/data/pg.json +1 -0
  192. data/lib/google_i18n/data/ph.json +1 -0
  193. data/lib/google_i18n/data/pk.json +1 -0
  194. data/lib/google_i18n/data/pl.json +1 -0
  195. data/lib/google_i18n/data/pm.json +1 -0
  196. data/lib/google_i18n/data/pn.json +1 -0
  197. data/lib/google_i18n/data/pr.json +1 -0
  198. data/lib/google_i18n/data/ps.json +1 -0
  199. data/lib/google_i18n/data/pt.json +1 -0
  200. data/lib/google_i18n/data/pw.json +1 -0
  201. data/lib/google_i18n/data/py.json +1 -0
  202. data/lib/google_i18n/data/qa.json +1 -0
  203. data/lib/google_i18n/data/re.json +1 -0
  204. data/lib/google_i18n/data/ro.json +1 -0
  205. data/lib/google_i18n/data/rs.json +1 -0
  206. data/lib/google_i18n/data/ru.json +1 -0
  207. data/lib/google_i18n/data/rw.json +1 -0
  208. data/lib/google_i18n/data/sa.json +1 -0
  209. data/lib/google_i18n/data/sb.json +1 -0
  210. data/lib/google_i18n/data/sc.json +1 -0
  211. data/lib/google_i18n/data/sd.json +1 -0
  212. data/lib/google_i18n/data/se.json +1 -0
  213. data/lib/google_i18n/data/sg.json +1 -0
  214. data/lib/google_i18n/data/sh.json +1 -0
  215. data/lib/google_i18n/data/si.json +1 -0
  216. data/lib/google_i18n/data/sj.json +1 -0
  217. data/lib/google_i18n/data/sk.json +1 -0
  218. data/lib/google_i18n/data/sl.json +1 -0
  219. data/lib/google_i18n/data/sm.json +1 -0
  220. data/lib/google_i18n/data/sn.json +1 -0
  221. data/lib/google_i18n/data/so.json +1 -0
  222. data/lib/google_i18n/data/sr.json +1 -0
  223. data/lib/google_i18n/data/ss.json +1 -0
  224. data/lib/google_i18n/data/st.json +1 -0
  225. data/lib/google_i18n/data/sv.json +1 -0
  226. data/lib/google_i18n/data/sx.json +1 -0
  227. data/lib/google_i18n/data/sy.json +1 -0
  228. data/lib/google_i18n/data/sz.json +1 -0
  229. data/lib/google_i18n/data/ta.json +1 -0
  230. data/lib/google_i18n/data/tc.json +1 -0
  231. data/lib/google_i18n/data/td.json +1 -0
  232. data/lib/google_i18n/data/tf.json +1 -0
  233. data/lib/google_i18n/data/tg.json +1 -0
  234. data/lib/google_i18n/data/th.json +1 -0
  235. data/lib/google_i18n/data/tj.json +1 -0
  236. data/lib/google_i18n/data/tk.json +1 -0
  237. data/lib/google_i18n/data/tl.json +1 -0
  238. data/lib/google_i18n/data/tm.json +1 -0
  239. data/lib/google_i18n/data/tn.json +1 -0
  240. data/lib/google_i18n/data/to.json +1 -0
  241. data/lib/google_i18n/data/tr.json +1 -0
  242. data/lib/google_i18n/data/tt.json +1 -0
  243. data/lib/google_i18n/data/tv.json +1 -0
  244. data/lib/google_i18n/data/tw.json +1 -0
  245. data/lib/google_i18n/data/tz.json +1 -0
  246. data/lib/google_i18n/data/ua.json +1 -0
  247. data/lib/google_i18n/data/ug.json +1 -0
  248. data/lib/google_i18n/data/um.json +1 -0
  249. data/lib/google_i18n/data/us.json +1 -0
  250. data/lib/google_i18n/data/uy.json +1 -0
  251. data/lib/google_i18n/data/uz.json +1 -0
  252. data/lib/google_i18n/data/va.json +1 -0
  253. data/lib/google_i18n/data/vc.json +1 -0
  254. data/lib/google_i18n/data/ve.json +1 -0
  255. data/lib/google_i18n/data/vg.json +1 -0
  256. data/lib/google_i18n/data/vi.json +1 -0
  257. data/lib/google_i18n/data/vn.json +1 -0
  258. data/lib/google_i18n/data/vu.json +1 -0
  259. data/lib/google_i18n/data/wf.json +1 -0
  260. data/lib/google_i18n/data/ws.json +1 -0
  261. data/lib/google_i18n/data/xk.json +1 -0
  262. data/lib/google_i18n/data/ye.json +1 -0
  263. data/lib/google_i18n/data/yt.json +1 -0
  264. data/lib/google_i18n/data/za.json +1 -0
  265. data/lib/google_i18n/data/zm.json +1 -0
  266. data/lib/google_i18n/data/zw.json +1 -0
  267. data/lib/google_i18n/data/zz.json +1 -0
  268. data/lib/google_i18n/validation_rules.rb +31 -0
  269. data/lib/google_i18n/version.rb +3 -0
  270. metadata +397 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2ec23d03d53d26a896f98d7fabc66aca0c4746f2ed186726f3f539f7a0596861
4
+ data.tar.gz: 9140529284abd1d9baaf8a892f819be6488eed5b9b90dfd1194e18590a604fdf
5
+ SHA512:
6
+ metadata.gz: b4f5fd00aa37ff3af5c0ff170a89d393a16314818627f4327419890c4591504e897642f844bd787f19cf9b1c8a4d65202c0019d6248aee6ceb3951aeac95c94f
7
+ data.tar.gz: 8c17dc7c93918202286e304be5f971c4003ad996a3473562bc230516298f30a9dffbf9fa52483d9f0840e39c9defb7e6ccd16d0e579c0c2878bf42ffc800c6d2
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.3
7
+ before_install: gem install bundler -v 1.17.2
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in google_i18n.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,57 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ google_i18n (0.1.3)
5
+ activesupport
6
+ json
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activesupport (6.1.3.1)
12
+ concurrent-ruby (~> 1.0, >= 1.0.2)
13
+ i18n (>= 1.6, < 2)
14
+ minitest (>= 5.1)
15
+ tzinfo (~> 2.0)
16
+ zeitwerk (~> 2.3)
17
+ coderay (1.1.3)
18
+ concurrent-ruby (1.1.8)
19
+ diff-lcs (1.4.4)
20
+ i18n (1.8.10)
21
+ concurrent-ruby (~> 1.0)
22
+ json (2.5.1)
23
+ method_source (1.0.0)
24
+ minitest (5.14.4)
25
+ pry (0.13.1)
26
+ coderay (~> 1.1)
27
+ method_source (~> 1.0)
28
+ rake (10.5.0)
29
+ rspec (3.10.0)
30
+ rspec-core (~> 3.10.0)
31
+ rspec-expectations (~> 3.10.0)
32
+ rspec-mocks (~> 3.10.0)
33
+ rspec-core (3.10.1)
34
+ rspec-support (~> 3.10.0)
35
+ rspec-expectations (3.10.1)
36
+ diff-lcs (>= 1.2.0, < 2.0)
37
+ rspec-support (~> 3.10.0)
38
+ rspec-mocks (3.10.2)
39
+ diff-lcs (>= 1.2.0, < 2.0)
40
+ rspec-support (~> 3.10.0)
41
+ rspec-support (3.10.2)
42
+ tzinfo (2.0.4)
43
+ concurrent-ruby (~> 1.0)
44
+ zeitwerk (2.4.2)
45
+
46
+ PLATFORMS
47
+ ruby
48
+
49
+ DEPENDENCIES
50
+ bundler (~> 1.17)
51
+ google_i18n!
52
+ pry (~> 0.13.1)
53
+ rake (~> 10.0)
54
+ rspec (~> 3.0)
55
+
56
+ BUNDLED WITH
57
+ 1.17.3
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # Google I18n Address
2
+
3
+ This gem provides I18n address normalization and validation based on
4
+ [Google's i18n address repository](https://chromium-i18n.appspot.com/ssl-address).
5
+
6
+ Code was ported to Ruby from this
7
+ [Python package](https://github.com/mirumee/google-i18n-address).
8
+
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'google_i18n'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install google_i18n
25
+
26
+ ## Usage
27
+
28
+ Initialize with some combination of the following:
29
+ - `country_code` The two-letter ISO 3166-1 country code
30
+ - `street_address` The street address
31
+ - `city` The city or town
32
+ - `city_area` The sublocality or district
33
+ - `name` A person's name
34
+ - `company_name` The name of a company or organization
35
+ - `country_area` The region, province or state
36
+ - `sorting_code` The sorting code
37
+ - `postal_code` The postal code or zip code
38
+
39
+ ### Normalization
40
+
41
+ ```
42
+ address = {country_code: 'US', country_area: 'California', city: 'Mountain View', postal_code: '94043', street_address: '1600 Amphitheatre Pkwy'}
43
+ a = GoogleI18n::Address.new(address)
44
+ a.normalize
45
+ => #<GoogleI18n::Address @country_code="US", @country_area="CA", @city="MOUNTAIN VIEW", @city_area="", @postal_code="94043", @street_address="1600 Amphitheatre Pkwy", @sorting_code="">
46
+ a.to_s
47
+ => "1600 Amphitheatre Pkwy\nMOUNTAIN VIEW , CA 94043\nUNITED STATES"
48
+ a.field_order
49
+ => [["name"], ["company_name"], ["street_address"], ["city", "country_area", "postal_code"], ["country_name"]]
50
+
51
+ address = {country_code: 'CN', 'country_area': 'Beijing', 'city': 'Chaoyang', 'postal_code': '100600', street_address: '55 Anjialou Rd'}
52
+ GoogleI18n::Address.new(address).normalize
53
+ => #<GoogleI18n::Address @country_code="CN", @street_address="55 Anjialou Rd", @city="朝阳区", @country_area="北京市", @postal_code="100600", @city_area="", @sorting_code="">
54
+ ```
55
+
56
+ ### Latinization
57
+
58
+ ```
59
+ address = {'country_code': 'CN', 'country_area': '北京市', 'city': '朝阳区', 'city_area': '三元桥', 'street_address': '安家楼路55号', 'postal_code': '100600'}
60
+ GoogleI18n::Address.new(address).latinize
61
+ => #<GoogleI18n::Address @country_code="CN", @country_area="Beijing Shi", @city="Chaoyang Qu", @city_area="三元桥", @postal_code="100600", @street_address="安家楼路55号", @sorting_code="">
62
+ GoogleI18n::Address.new(address).latinize.normalize
63
+ => #<GoogleI18n::Address @country_code="CN", @street_address="55 Anjialou Rd", @city="朝阳区", @country_area="北京市", @postal_code="100600", @city_area="", @sorting_code="">
64
+ ```
65
+
66
+ ### Validation
67
+
68
+ ```
69
+ GoogleI18n::Address.new({country_code: 'US'}).normalize
70
+ => GoogleI18n::InvalidAddress (Invalid address {"country_area"=>"required", "city"=>"required", "postal_code"=>"required", "street_address"=>"required"})
71
+ ```
72
+
73
+ ### Country Areas
74
+
75
+ ```
76
+ GoogleI18n::Address.new({country_code: 'US'}).country_area_choices
77
+ => [['AL', 'Alabama'], ..., ['WY', 'Wyoming']]
78
+
79
+ ```
80
+
81
+ ## Development
82
+
83
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
84
+
85
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
86
+
87
+ ## Contributing
88
+
89
+ Bug reports and pull requests are welcome on GitLab at https://gitlab.com/dimitridee/google_i18n
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "google_i18n"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,45 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "google_i18n/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "google_i18n"
8
+ spec.version = GoogleI18n::VERSION
9
+ spec.authors = ["Dimitri D"]
10
+ spec.email = ["demonitri@gmail.com"]
11
+
12
+ spec.summary = %q{i18n address standardization and validation using Google's i18n address repo}
13
+ # spec.description = %q{TODO: Write a longer description or delete this line.}
14
+ spec.homepage = "https://gitlab.com/dimitri_dee/google_i18n"
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+ if spec.respond_to?(:metadata)
19
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
20
+
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = spec.homepage
23
+ spec.metadata["changelog_uri"] = spec.homepage
24
+ else
25
+ raise "RubyGems 2.0 or newer is required to protect against " \
26
+ "public gem pushes."
27
+ end
28
+
29
+ # Specify which files should be added to the gem when it is released.
30
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
31
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
32
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
33
+ end
34
+ spec.bindir = "exe"
35
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
36
+ spec.require_paths = ["lib"]
37
+
38
+ spec.add_development_dependency "bundler", "~> 1.17"
39
+ spec.add_development_dependency "rake", "~> 10.0"
40
+ spec.add_development_dependency "rspec", "~> 3.0"
41
+ spec.add_development_dependency "pry", "~> 0.13.1"
42
+
43
+ spec.add_dependency 'json'
44
+ spec.add_dependency "activesupport"
45
+ end
@@ -0,0 +1,12 @@
1
+ require "json"
2
+ require "active_support/core_ext/object/blank"
3
+ require "active_support/ordered_hash"
4
+
5
+ module GoogleI18n
6
+ class InvalidAddress < StandardError; end
7
+ class Error < StandardError; end
8
+
9
+ require "google_i18n/version"
10
+ require "google_i18n/validation_rules"
11
+ require "google_i18n/address"
12
+ end
@@ -0,0 +1,495 @@
1
+ module GoogleI18n
2
+ class Address
3
+ VALID_COUNTRY_CODE = %r(^\w{2,3}$)
4
+ VALIDATION_DATA_DIR = File.join(File.dirname(__FILE__), "data")
5
+ VALIDATION_DATA_PATH = File.join(VALIDATION_DATA_DIR, "%s.json")
6
+
7
+ FIELD_MAPPING = {
8
+ "A" => "street_address",
9
+ "C" => "city",
10
+ "D" => "city_area",
11
+ "N" => "name",
12
+ "O" => "company_name",
13
+ "S" => "country_area",
14
+ "X" => "sorting_code",
15
+ "Z" => "postal_code",
16
+ }
17
+
18
+ KNOWN_FIELDS = FIELD_MAPPING.values.push("country_code")
19
+
20
+ attr_accessor :country_code, :street_address, :city, :city_area, :name, :company_name,
21
+ :country_area, :sorting_code, :postal_code
22
+
23
+ # Creates a new Address object from component parts
24
+ #
25
+ # @option options [String] :country_code The two-letter ISO 3166-1 country code
26
+ # @option options [String] :street_address The street address
27
+ # @option options [String] :city The city or town
28
+ # @option options [String] :city_area The sublocality or district
29
+ # @option options [String] :name A person's name
30
+ # @option options [String] :company_name The name of a company or organization
31
+ # @option options [String] :country_area The region, province or state
32
+ # @option options [String] :sorting_code The sorting code
33
+ # @option options [String] :postal_code The postal code or zip code
34
+ #
35
+ # @return [GoogleI18n::Address] The constructed Address
36
+ def initialize(options={})
37
+ @country_code = options[:country_code] if options[:country_code]
38
+ @street_address = options[:street_address] if options[:street_address]
39
+ @city = options[:city] if options[:city]
40
+ @city_area = options[:city_area] if options[:city_area]
41
+ @name = options[:name] if options[:name]
42
+ @company_name = options[:company_name] if options[:company_name]
43
+ @country_area = options[:country_area] if options[:country_area]
44
+ @sorting_code = options[:sorting_code] if options[:sorting_code]
45
+ @postal_code = options[:postal_code] if options[:postal_code]
46
+ end
47
+
48
+ def dup
49
+ self.class.new(
50
+ country_code: self.country_code ? self.country_code.dup : nil,
51
+ street_address: self.street_address ? self.street_address.dup : nil,
52
+ city: self.city ? self.city.dup : nil,
53
+ city_area: self.city_area ? self.city_area.dup : nil,
54
+ name: self.name ? self.name.dup : nil,
55
+ company_name: self.company_name ? self.company_name.dup : nil,
56
+ country_area: self.country_area ? self.country_area.dup : nil,
57
+ sorting_code: self.sorting_code ? self.sorting_code.dup : nil,
58
+ postal_code: self.postal_code ? self.postal_code.dup : nil,
59
+ )
60
+ end
61
+
62
+ def to_h
63
+ {
64
+ country_code: self.country_code,
65
+ street_address: self.street_address,
66
+ city: self.city,
67
+ city_area: self.city_area,
68
+ name: self.name,
69
+ company_name: self.company_name,
70
+ country_area: self.country_area,
71
+ sorting_code: self.sorting_code,
72
+ postal_code: self.postal_code,
73
+ }
74
+ end
75
+
76
+ # Displays the address in a newline delimited format suitable for printing
77
+ #
78
+ # @param normalize [Boolean] If true, address will be normalized first
79
+ # @param latin [Boolean] Show the address fields in Latinized order
80
+ #
81
+ # @return [String]
82
+ def to_s(normalize: true, latin: false)
83
+ cleaned_data = address.dup
84
+ cleaned_data = cleaned_data.normalize if normalize
85
+
86
+ rules = get_validation_rules
87
+
88
+ address_format = if latin
89
+ rules.address_latin_format
90
+ else
91
+ rules.address_format
92
+ end
93
+
94
+ address_line_formats = address_format.split("%n")
95
+
96
+ address_lines = address_line_formats.inject([]) do |m, line_format|
97
+ m << format_address_line(line_format, cleaned_data, rules)
98
+ m
99
+ end
100
+
101
+ address_lines << rules.country_name
102
+ address_lines.keep_if(&:present?)
103
+ address_lines.join("\n")
104
+ end
105
+
106
+ # Returns an array where each element is an array representing the fields to include on each address line, e.g:
107
+ # [["name"], ["company_name"], ["street_address"], ["city", "country_area", "postal_code"], ["country_name"]]
108
+ #
109
+ # @param latin [Boolean] Show the address fields in Latinized order
110
+ #
111
+ # @return [Array<Array<String>>]
112
+ def field_order(latin: false)
113
+ rules = get_validation_rules
114
+
115
+ address_format = if latin
116
+ rules.address_latin_format
117
+ else
118
+ rules.address_format
119
+ end
120
+
121
+ address_lines = address_format.split("%n")
122
+
123
+ replacements = FIELD_MAPPING.inject({}) do |m, (code, field_name)|
124
+ m["%#{code}"] = field_name
125
+ m
126
+ end
127
+
128
+ all_lines = []
129
+ address_lines.each do |line|
130
+ fields = line.split(%r{(%.)})
131
+ single_line = fields.map {|field| replacements[field]}.compact
132
+ all_lines << single_line
133
+ end
134
+
135
+ all_lines << Array('country_name')
136
+ end
137
+
138
+ # Returns an array where each element is an array representing the key and value for each country area. e.g:
139
+ # [['AL', 'Alabama'], ..., ['WY', 'Wyoming']]
140
+ def country_area_choices
141
+ get_validation_rules.country_area_choices
142
+ end
143
+
144
+ # Format the address in a way suitable for use in its part of the world
145
+ #
146
+ # @return [GoogleI18n::Address]
147
+ def normalize
148
+ errors = {}
149
+ # begin
150
+ rules = get_validation_rules
151
+ # rescue StandardError
152
+ # errors["country_code"] = "invalid"
153
+ # end
154
+
155
+ if errors.blank?
156
+ cleaned_data = address.dup
157
+ country_code = cleaned_data.country_code
158
+ if country_code.blank?
159
+ errors["country_code"] = "required"
160
+ else
161
+ cleaned_data.country_code = country_code.upcase
162
+ end
163
+
164
+ cleaned_data.country_area, errors = normalize_field("country_area", rules, cleaned_data, rules.country_area_choices, errors)
165
+ cleaned_data.city, errors = normalize_field("city", rules, cleaned_data, rules.city_choices, errors)
166
+ cleaned_data.city_area, errors = normalize_field("city_area", rules, cleaned_data, rules.city_area_choices, errors)
167
+ cleaned_data.postal_code, errors = normalize_field("postal_code", rules, cleaned_data, [], errors)
168
+
169
+ postal_code = cleaned_data.postal_code
170
+ if rules.postal_code_matchers.present? && postal_code.present?
171
+ rules.postal_code_matchers.each do |matcher|
172
+ if !matcher.match(postal_code)
173
+ errors["postal_code"] = "invalid"
174
+ break
175
+ end
176
+ end
177
+ end
178
+
179
+ cleaned_data.street_address, errors = normalize_field("street_address", rules, cleaned_data, [], errors)
180
+ cleaned_data.sorting_code, errors = normalize_field("sorting_code", rules, cleaned_data, [], errors)
181
+ cleaned_data
182
+ end
183
+
184
+ if errors.present?
185
+ raise InvalidAddress, "Invalid address #{errors}"
186
+ end
187
+
188
+ cleaned_data
189
+ end
190
+
191
+ # Format the address in a way suitable for use in Latin alphabets.
192
+ # If a latinized version is not available, the local version will be used instead.
193
+ #
194
+ # @param normalize [Boolean] If true, address will be normalized first
195
+ #
196
+ # @return [GoogleI18n::Address]
197
+ def latinize(normalize: true)
198
+ cleaned_data = address.dup
199
+ cleaned_data = cleaned_data.normalize if normalize
200
+
201
+ country_code = cleaned_data.country_code&.upcase
202
+ dummy_country_data, database = load_country_data(country_code)
203
+
204
+ if country_code.present?
205
+ country_area = cleaned_data.country_area
206
+ if country_area.present?
207
+ key = "%s/%s" % [country_code, country_area]
208
+ country_area_data = database[key]
209
+ if country_area_data.present?
210
+ cleaned_data.country_area = country_area_data.fetch("lname", country_area_data.fetch("name", country_area))
211
+ city = cleaned_data.city
212
+ key = "%s/%s/%s" % [country_code, country_area, city]
213
+ city_data = database[key]
214
+ if city_data.present?
215
+ cleaned_data.city = city_data.fetch("lname", city_data.fetch("name", city))
216
+ city_area = cleaned_data.city_area
217
+ key = "%s/%s/%s/%s" % [country_code, country_area, city, city_area]
218
+ city_area_data = database[key]
219
+ if city_area_data.present?
220
+ cleaned_data.city_area = city_area_data.fetch("lname", city_area_data.fetch("name", city_area))
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ cleaned_data
228
+ end
229
+
230
+ private
231
+
232
+ def address
233
+ self
234
+ end
235
+
236
+ def format_address_line(line_format, address, rules)
237
+ replacements = FIELD_MAPPING.inject({}) do |m, (code, field_name)|
238
+ value = address.send(field_name)
239
+ value = value.upcase if rules.upper_fields.include?(field_name)
240
+ m["%#{code}"] = value
241
+ m
242
+ end
243
+
244
+ fields = line_format.split(%r{(%.)})
245
+ fields = fields.map {|field| replacements.fetch(field, field)&.strip}
246
+ fields.keep_if(&:present?)
247
+ fields.join(' ')
248
+ end
249
+
250
+ def normalize_field(name, rules, data, choices, errors)
251
+ value = data.send(name)
252
+
253
+ if rules.upper_fields.include?(name) && value.present?
254
+ data.send("#{name}=", value.upcase)
255
+ end
256
+
257
+ if !rules.allowed_fields.include?(name)
258
+ data.send("#{name}=", "")
259
+ elsif value.blank?
260
+ if rules.required_fields.include?(name)
261
+ errors[name] = "required"
262
+ else
263
+ data.send("#{name}=", "")
264
+ end
265
+ elsif choices.present?
266
+ if value.present? || rules.required_fields.include?(name)
267
+ value = match_choices(value, choices)
268
+ if value.present?
269
+ data.send("#{name}=", value)
270
+ else
271
+ errors[name] = "invalid"
272
+ end
273
+ end
274
+ end
275
+
276
+ [data.send(name), errors]
277
+ end
278
+
279
+ def make_choices(rules, translated = false)
280
+ sub_keys = rules["sub_keys"]
281
+ return if sub_keys.blank?
282
+
283
+ choices = []
284
+ sub_keys = sub_keys.split("~")
285
+
286
+ if sub_names = rules["sub_names"]
287
+ choices += sub_keys.zip(sub_names.split("~"))
288
+ elsif !translated
289
+ choices += sub_keys.zip(sub_keys)
290
+ end
291
+
292
+ if !translated
293
+ if sub_lnames = rules["sub_lnames"]
294
+ choices += sub_keys.zip(sub_lnames.split("~"))
295
+ end
296
+
297
+ if sub_lfnames = rules["sub_lfnames"]
298
+ choices += sub_keys.zip(sub_lfnames.split("~"))
299
+ end
300
+ end
301
+
302
+ choices
303
+ end
304
+
305
+ # This returns the regional form of `value`.
306
+ # `choices` looks something like this: [['安徽省', 'Anhui Sheng'], ['安徽省', '安徽省']]
307
+ # the idea is that whether `value` is 'Anhui Sheng' or '安徽省', it will return '安徽省'
308
+ def match_choices(value, choices)
309
+ return if value.blank?
310
+ value = value.strip.downcase
311
+ # using `match` here rather than `==` because we want them to be able to use "New York" and "New York City"
312
+ matched = choices.select { |name, label| name&.downcase.to_s.match(value) || label&.downcase.to_s.match(value) }
313
+ matched.flatten.first
314
+ end
315
+
316
+ # This returns something like:
317
+ # [['AL', 'Alabama'], ['AK', 'Alaska'], ['AS', 'American Samoa'], ['AZ', 'Arizona'], ...]
318
+ # or for non-latin alphabets:
319
+ # [['安徽省', 'Anhui Sheng'], ['安徽省', '安徽省'], ['澳门', 'Macau'], ['澳门', '澳门'], ['北京市', 'Beijing Shi'], ['北京市', '北京市'], ...]
320
+ def compact_choices(choices)
321
+ value_map = ActiveSupport::OrderedHash.new {|hsh, key| hsh[key] = [] }
322
+ choices.each do |key, value|
323
+ value_map[key] << value.to_s
324
+ end
325
+
326
+ ret = []
327
+ value_map.each do |k,vv|
328
+ vv.sort.each do |v|
329
+ ret << [k,v]
330
+ end
331
+ end
332
+
333
+ ret
334
+ end
335
+
336
+ def get_validation_rules
337
+ country_code = address.country_code&.upcase
338
+ country_data, database = load_country_data(country_code)
339
+ country_name = country_data.fetch("name", "")
340
+ address_format = country_data["fmt"]
341
+ address_latin_format = country_data.fetch("lfmt", address_format)
342
+
343
+ allowed_fields = address_format.scan(%r{%([#{FIELD_MAPPING.keys.join}])}).flatten
344
+ allowed_fields = FIELD_MAPPING.fetch_values(*allowed_fields)
345
+ required_fields = FIELD_MAPPING.fetch_values(*country_data["require"].split(//))
346
+ upper_fields = FIELD_MAPPING.fetch_values(*country_data["upper"].split(//))
347
+
348
+ languages = country_data["languages"]&.split("~") || []
349
+
350
+ postal_code_matchers = []
351
+ if allowed_fields.include?("postal_code") && country_data.has_key?("zip")
352
+ postal_code_matchers << %r(^#{country_data["zip"]}$)
353
+ end
354
+
355
+ postal_code_examples = country_data["zipex"]&.split(",") || []
356
+
357
+ country_area_choices = []
358
+ city_choices = []
359
+ city_area_choices = []
360
+ country_area_type = country_data["state_name_type"]
361
+ city_type = country_data["locality_name_type"]
362
+ city_area_type = country_data["sublocality_name_type"]
363
+ postal_code_type = country_data["zip_name_type"]
364
+ postal_code_prefix = country_data.fetch("postprefix", "")
365
+
366
+ # second level of data is for administrative areas
367
+ country_area = nil
368
+ city = nil
369
+ city_area = nil
370
+ if database.has_key?(country_code)
371
+ if country_data.has_key?("sub_keys")
372
+ languages.each do |language|
373
+ is_default_language = (language.blank? || language == country_data["lang"])
374
+ matched_country_area = nil
375
+ matched_city = nil
376
+ if is_default_language
377
+ localized_country_data = database[country_code]
378
+ else
379
+ localized_country_data = database["#{country_code}--#{language}"]
380
+ end
381
+
382
+ localized_country_area_choices = make_choices(localized_country_data)
383
+ country_area_choices += localized_country_area_choices
384
+ existing_choice = country_area.present?
385
+ matched_country_area = country_area = match_choices(address.country_area, localized_country_area_choices)
386
+
387
+ # third level of data is for cities
388
+ if matched_country_area.present?
389
+ if is_default_language
390
+ country_area_data = database["#{country_code}/#{country_area}"]
391
+ else
392
+ country_area_data = database["#{country_code}/#{country_area}--#{language}"]
393
+ end
394
+
395
+ if existing_choice.blank?
396
+ if country_area_data.include?("zip")
397
+ postal_code_matchers << %r(^#{country_area_data["zip"]})
398
+ end
399
+
400
+ if country_area_data.include?("zipex")
401
+ postal_code_examples = country_area_data["zipex"].split(",")
402
+ end
403
+ end
404
+
405
+ if country_area_data.include?("sub_keys")
406
+ localized_city_choices = make_choices(country_area_data)
407
+ city_choices += localized_city_choices
408
+ existing_choice = city.present?
409
+ matched_city = city = match_choices(address.city, localized_city_choices)
410
+ end
411
+
412
+ # fourth level of data is for dependent sublocalities
413
+ if matched_city.present?
414
+ if is_default_language
415
+ city_data = database["#{country_code}/#{country_area}/#{city}"]
416
+ else
417
+ city_data = database["#{country_code}/#{country_area}/#{city}--#{language}"]
418
+ end
419
+
420
+ if existing_choice.blank?
421
+ if city_data.include?("zip")
422
+ postal_code_matchers << %r(^#{city_data["zip"]})
423
+ end
424
+
425
+ if city_data.include?("zipex")
426
+ postal_code_examples = city_data["zipex"].split(",")
427
+ end
428
+ end
429
+
430
+ if city_data.include?("sub_keys")
431
+ localized_city_area_choices = make_choices(city_data)
432
+ city_area_choices += localized_city_area_choices
433
+ existing_choice = city_area.present?
434
+ matched_city_area = city_area = match_choices(address.city_area, localized_city_area_choices)
435
+ if matched_city_area.present?
436
+ if is_default_language
437
+ city_area_data = database["#{country_code}/#{country_area}/#{city}/#{city_area}"]
438
+ else
439
+ city_area_data = database["#{country_code}/#{country_area}/#{city}/#{city_area}--#{language}"]
440
+ end
441
+
442
+ if existing_choice.blank?
443
+ if city_area_data.include?("zip")
444
+ postal_code_matchers << %r(^#{city_area_data["zip"]})
445
+ end
446
+ if city_area_data.include?("zipex")
447
+ postal_code_examples = city_area_data["zipex"].split(",")
448
+ end
449
+ end
450
+ end
451
+ end
452
+ end
453
+ end
454
+ end
455
+ country_area_choices = compact_choices(country_area_choices)
456
+ city_choices = compact_choices(city_choices)
457
+ city_area_choices = compact_choices(city_area_choices)
458
+ end
459
+ end
460
+
461
+ ValidationRules.new(country_code, country_name, address_format, address_latin_format,
462
+ allowed_fields, required_fields, upper_fields, country_area_type,
463
+ country_area_choices, city_type, city_choices, city_area_type, city_area_choices,
464
+ postal_code_type, postal_code_matchers, postal_code_examples, postal_code_prefix)
465
+ end
466
+
467
+ def load_validation_data(country_code="all")
468
+ if !VALID_COUNTRY_CODE.match(country_code)
469
+ raise "#{country_code} is not a valid country code"
470
+ end
471
+
472
+ path = VALIDATION_DATA_PATH % country_code.downcase
473
+ if !File.exist?(path)
474
+ raise "No metadata file exists at path: #{path}"
475
+ end
476
+
477
+ JSON.parse(File.read(path))
478
+ end
479
+
480
+ def load_country_data(country_code)
481
+ # This ZZ stuff is just defaults to init with
482
+ database = load_validation_data("zz")
483
+ country_data = database["ZZ"]
484
+ if country_code
485
+ country_code = country_code.upcase
486
+ if country_code.downcase == "zz"
487
+ raise "'#{country_code}' is not a valid country code"
488
+ end
489
+ database = load_validation_data(country_code.downcase)
490
+ country_data.merge!(database[country_code.upcase])
491
+ end
492
+ [country_data, database]
493
+ end
494
+ end
495
+ end