google_i18n 0.1.3

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 (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