i18n-js 4.0.1 → 4.1.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.
@@ -0,0 +1,196 @@
1
+ import { readFileSync, statSync } from "fs";
2
+ import * as ts from "typescript";
3
+ import { glob } from "glob";
4
+
5
+ type ScopeInfo = {
6
+ type: "default" | "scope" | "base";
7
+ location: string;
8
+ base: string | null;
9
+ full: string;
10
+ scope: string;
11
+ };
12
+
13
+ function location(node: ts.Node, append: string = ":") {
14
+ const sourceFile = node.getSourceFile();
15
+ let { line, character } = sourceFile.getLineAndCharacterOfPosition(
16
+ node.getStart(sourceFile)
17
+ );
18
+
19
+ line += 1;
20
+ character += 1;
21
+ const file = sourceFile.fileName;
22
+ const location = `${file}:${line}:${character}`;
23
+
24
+ return `${location}${append}`;
25
+ }
26
+
27
+ const callExpressions = ["t", "i18n.t", "i18n.translate"];
28
+
29
+ function tsKind(node: ts.Node) {
30
+ const keys = Object.keys(ts.SyntaxKind);
31
+ const values = Object.values(ts.SyntaxKind);
32
+
33
+ return keys[values.indexOf(node.kind)];
34
+ }
35
+
36
+ function getTranslationScopesFromFile(filePath: string) {
37
+ const scopes: ScopeInfo[] = [];
38
+
39
+ const sourceFile = ts.createSourceFile(
40
+ filePath,
41
+ readFileSync(filePath).toString(),
42
+ ts.ScriptTarget.ES2015,
43
+ true
44
+ );
45
+
46
+ inspect(sourceFile);
47
+
48
+ return scopes;
49
+
50
+ function inspect(node: ts.Node) {
51
+ const next = () => {
52
+ ts.forEachChild(node, inspect);
53
+ };
54
+
55
+ if (node.kind !== ts.SyntaxKind.CallExpression) {
56
+ return next();
57
+ }
58
+
59
+ const expr = node.getChildAt(0).getText();
60
+ const text = JSON.stringify(node.getText(sourceFile));
61
+
62
+ if (!callExpressions.includes(expr)) {
63
+ return next();
64
+ }
65
+
66
+ const syntaxList = node.getChildAt(2);
67
+
68
+ if (!syntaxList.getText().trim()) {
69
+ return next();
70
+ }
71
+
72
+ const scopeNode = syntaxList.getChildAt(0) as ts.StringLiteral;
73
+ const optionsNode = syntaxList.getChildAt(2) as ts.ObjectLiteralExpression;
74
+
75
+ if (scopeNode.kind !== ts.SyntaxKind.StringLiteral) {
76
+ return next();
77
+ }
78
+
79
+ if (
80
+ optionsNode &&
81
+ optionsNode.kind !== ts.SyntaxKind.ObjectLiteralExpression
82
+ ) {
83
+ return next();
84
+ }
85
+
86
+ if (!optionsNode) {
87
+ scopes.push({
88
+ type: "scope",
89
+ scope: scopeNode.text,
90
+ base: null,
91
+ full: scopeNode.text,
92
+ location: location(node, ""),
93
+ });
94
+ return next();
95
+ }
96
+
97
+ scopes.push(...getScopes(scopeNode, optionsNode));
98
+ }
99
+
100
+ function mapProperties(node: ts.ObjectLiteralExpression): {
101
+ name: string;
102
+ value: ts.Node;
103
+ }[] {
104
+ return node.properties.map((p) => ({
105
+ name: (p.name as ts.Identifier).escapedText.toString(),
106
+ value: p.getChildAt(2),
107
+ }));
108
+ }
109
+
110
+ function getScopes(
111
+ scopeNode: ts.StringLiteral,
112
+ node: ts.ObjectLiteralExpression
113
+ ): ScopeInfo[] {
114
+ const suffix = scopeNode.text;
115
+
116
+ const result: ScopeInfo[] = [];
117
+ const properties = mapProperties(node);
118
+
119
+ if (
120
+ properties.length === 0 ||
121
+ !properties.some((p) => p.name === "scope")
122
+ ) {
123
+ result.push({
124
+ type: "scope",
125
+ scope: suffix,
126
+ base: null,
127
+ full: suffix,
128
+ location: location(scopeNode, ""),
129
+ });
130
+ }
131
+
132
+ properties.forEach((property) => {
133
+ if (
134
+ property.name === "scope" &&
135
+ property.value.kind === ts.SyntaxKind.StringLiteral
136
+ ) {
137
+ const base = (property.value as ts.StringLiteral).text;
138
+
139
+ result.push({
140
+ type: "base",
141
+ scope: suffix,
142
+ base,
143
+ full: `${base}.${suffix}`,
144
+ location: location(scopeNode, ""),
145
+ });
146
+ }
147
+
148
+ if (
149
+ property.name === "defaults" &&
150
+ property.value.kind === ts.SyntaxKind.ArrayLiteralExpression
151
+ ) {
152
+ const op = property.value as ts.ArrayLiteralExpression;
153
+ const values = op.getChildAt(1);
154
+ const objects = (
155
+ values
156
+ .getChildren()
157
+ .filter(
158
+ (n) => n.kind === ts.SyntaxKind.ObjectLiteralExpression
159
+ ) as ts.ObjectLiteralExpression[]
160
+ ).map(mapProperties);
161
+
162
+ objects.forEach((object) => {
163
+ object.forEach((prop) => {
164
+ if (
165
+ prop.name === "scope" &&
166
+ prop.value.kind === ts.SyntaxKind.StringLiteral
167
+ ) {
168
+ const text = (prop.value as ts.StringLiteral).text;
169
+
170
+ result.push({
171
+ type: "default",
172
+ scope: text,
173
+ base: null,
174
+ full: text,
175
+ location: location(prop.value, ""),
176
+ });
177
+ }
178
+ });
179
+ });
180
+ }
181
+ });
182
+
183
+ return result;
184
+ }
185
+ }
186
+
187
+ const patterns = (
188
+ process.argv[2] ??
189
+ "!(node_modules)/**/*.js:!(node_modules)/**/*.ts:!(node_modules)/**/*.jsx:!(node_modules)/**/*.tsx"
190
+ ).split(":");
191
+ const files = patterns.flatMap((pattern) => glob.sync(pattern));
192
+ const scopes = files
193
+ .filter((filePath) => statSync(filePath).isFile())
194
+ .flatMap((path) => getTranslationScopesFromFile(path));
195
+
196
+ console.log(JSON.stringify(scopes, null, 2));
@@ -8,6 +8,7 @@ module I18nJS
8
8
  def self.listen(
9
9
  config_file: Rails.root.join("config/i18n.yml"),
10
10
  locales_dir: Rails.root.join("config/locales"),
11
+ run_on_start: true,
11
12
  options: {}
12
13
  )
13
14
  return unless Rails.env.development?
@@ -19,7 +20,7 @@ module I18nJS
19
20
 
20
21
  self.started = true
21
22
 
22
- locales_dirs = Array(locales_dir)
23
+ locales_dirs = Array(locales_dir).map {|path| File.expand_path(path) }
23
24
 
24
25
  relative_paths =
25
26
  [config_file, *locales_dirs].map {|path| relative_path(path) }
@@ -27,7 +28,7 @@ module I18nJS
27
28
  debug("Watching #{relative_paths.inspect}")
28
29
 
29
30
  listener(config_file, locales_dirs.map(&:to_s), options).start
30
- I18nJS.call(config_file: config_file)
31
+ I18nJS.call(config_file: config_file) if run_on_start
31
32
  end
32
33
 
33
34
  def self.relative_path(path)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "schema"
4
+
5
+ module I18nJS
6
+ def self.plugins
7
+ @plugins ||= []
8
+ end
9
+
10
+ def self.register_plugin(plugin)
11
+ plugins << plugin
12
+ plugin.setup
13
+ end
14
+
15
+ def self.plugin_files
16
+ Gem.find_files("i18n-js/*_plugin.rb")
17
+ end
18
+
19
+ def self.load_plugins!
20
+ plugin_files.each do |path|
21
+ require path
22
+ end
23
+ end
24
+
25
+ class Plugin
26
+ def self.transform(translations:, config:) # rubocop:disable Lint/UnusedMethodArgument
27
+ translations
28
+ end
29
+
30
+ # Must raise I18nJS::SchemaInvalidError with the error message if schema
31
+ # validation has failed.
32
+ def self.validate_schema(config:)
33
+ end
34
+
35
+ def self.setup
36
+ end
37
+ end
38
+ end
@@ -4,14 +4,28 @@ module I18nJS
4
4
  class Schema
5
5
  InvalidError = Class.new(StandardError)
6
6
 
7
- ROOT_KEYS = %i[translations check].freeze
8
- REQUIRED_ROOT_KEYS = %i[translations].freeze
9
- REQUIRED_CHECK_KEYS = %i[ignore].freeze
7
+ REQUIRED_LINT_TRANSLATIONS_KEYS = %i[ignore].freeze
8
+ REQUIRED_LINT_SCRIPTS_KEYS = %i[ignore patterns].freeze
10
9
  REQUIRED_TRANSLATION_KEYS = %i[file patterns].freeze
11
10
  TRANSLATION_KEYS = %i[file patterns].freeze
12
11
 
12
+ def self.root_keys
13
+ @root_keys ||= Set.new(%i[
14
+ translations
15
+ lint_translations
16
+ lint_scripts
17
+ check
18
+ ])
19
+ end
20
+
21
+ def self.required_root_keys
22
+ @required_root_keys ||= Set.new(%i[translations])
23
+ end
24
+
13
25
  def self.validate!(target)
14
- new(target).validate!
26
+ schema = new(target)
27
+ schema.validate!
28
+ I18nJS.plugins.each {|plugin| plugin.validate_schema(config: target) }
15
29
  end
16
30
 
17
31
  attr_reader :target
@@ -23,20 +37,36 @@ module I18nJS
23
37
  def validate!
24
38
  expect_type(:root, target, Hash, target)
25
39
 
26
- expect_required_keys(REQUIRED_ROOT_KEYS, target)
27
- reject_extraneous_keys(ROOT_KEYS, target)
40
+ expect_required_keys(self.class.required_root_keys, target)
41
+ reject_extraneous_keys(self.class.root_keys, target)
28
42
  validate_translations
29
- validate_check
43
+ validate_lint_translations
44
+ validate_lint_scripts
30
45
  end
31
46
 
32
- def validate_check
33
- return unless target.key?(:check)
47
+ def validate_lint_translations
48
+ key = :lint_translations
34
49
 
35
- check = target[:check]
50
+ return unless target.key?(key)
36
51
 
37
- expect_type(:check, check, Hash, target)
38
- expect_required_keys(REQUIRED_CHECK_KEYS, check)
39
- expect_type(:ignore, check[:ignore], Array, check)
52
+ config = target[key]
53
+
54
+ expect_type(key, config, Hash, target)
55
+ expect_required_keys(REQUIRED_LINT_TRANSLATIONS_KEYS, config)
56
+ expect_type(:ignore, config[:ignore], Array, config)
57
+ end
58
+
59
+ def validate_lint_scripts
60
+ key = :lint_scripts
61
+
62
+ return unless target.key?(key)
63
+
64
+ config = target[key]
65
+
66
+ expect_type(key, config, Hash, target)
67
+ expect_required_keys(REQUIRED_LINT_SCRIPTS_KEYS, config)
68
+ expect_type(:ignore, config[:ignore], Array, config)
69
+ expect_type(:patterns, config[:patterns], Array, config)
40
70
  end
41
71
 
42
72
  def validate_translations
@@ -63,6 +93,15 @@ module I18nJS
63
93
  raise InvalidError, "#{error_message}#{node_json}"
64
94
  end
65
95
 
96
+ def expect_enabled_config(config_key, value)
97
+ return if [TrueClass, FalseClass].include?(value.class)
98
+
99
+ actual_type = value.class
100
+
101
+ reject "Expected #{config_key}.enabled to be a boolean; " \
102
+ "got #{actual_type} instead"
103
+ end
104
+
66
105
  def expect_type(attribute, value, expected_type, payload)
67
106
  return if value.is_a?(expected_type)
68
107
 
@@ -94,7 +133,7 @@ module I18nJS
94
133
 
95
134
  def reject_extraneous_keys(allowed_keys, value)
96
135
  keys = value.keys.map(&:to_sym)
97
- extraneous = keys - allowed_keys
136
+ extraneous = keys.to_a - allowed_keys.to_a
98
137
 
99
138
  return if extraneous.empty?
100
139
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module I18nJS
4
- VERSION = "4.0.1"
4
+ VERSION = "4.1.0"
5
5
  end
data/lib/i18n-js.rb CHANGED
@@ -6,9 +6,13 @@ require "yaml"
6
6
  require "glob"
7
7
  require "fileutils"
8
8
  require "optparse"
9
+ require "erb"
10
+ require "set"
11
+ require "digest/md5"
9
12
 
10
13
  require_relative "i18n-js/schema"
11
14
  require_relative "i18n-js/version"
15
+ require_relative "i18n-js/plugin"
12
16
 
13
17
  module I18nJS
14
18
  MissingConfigError = Class.new(StandardError)
@@ -19,19 +23,27 @@ module I18nJS
19
23
  "you must set either `config_file` or `config`"
20
24
  end
21
25
 
22
- config = Glob::SymbolizeKeys.call(config || YAML.load_file(config_file))
26
+ load_plugins!
27
+
28
+ config = Glob::SymbolizeKeys.call(config || load_config_file(config_file))
29
+
23
30
  Schema.validate!(config)
24
31
  exported_files = []
25
32
 
26
33
  config[:translations].each do |group|
27
- exported_files += export_group(group)
34
+ exported_files += export_group(group, config)
28
35
  end
29
36
 
30
37
  exported_files
31
38
  end
32
39
 
33
- def self.export_group(group)
40
+ def self.export_group(group, config)
34
41
  filtered_translations = Glob.filter(translations, group[:patterns])
42
+ filtered_translations =
43
+ plugins.reduce(filtered_translations) do |buffer, plugin|
44
+ plugin.transform(translations: buffer, config: config)
45
+ end
46
+
35
47
  output_file_path = File.expand_path(group[:file])
36
48
  exported_files = []
37
49
 
@@ -70,4 +82,9 @@ module I18nJS
70
82
  translations
71
83
  end
72
84
  end
85
+
86
+ def self.load_config_file(config_file)
87
+ erb = ERB.new(File.read(config_file))
88
+ YAML.safe_load(erb.result(binding))
89
+ end
73
90
  end
data/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "dependencies": {
3
+ "esbuild": "^0.16.2",
4
+ "glob": "^8.0.3",
5
+ "typescript": "^4.9.4"
6
+ },
7
+ "scripts": {
8
+ "compile": "esbuild lib/i18n-js/lint.ts --bundle --platform=node --outfile=lib/i18n-js/lint.js --target=node16"
9
+ }
10
+ }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: i18n-js
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.1
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nando Vieira
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-25 00:00:00.000000000 Z
11
+ date: 2022-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: glob
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 0.4.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 0.4.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: i18n
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mocha
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: pry-meta
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -176,6 +190,7 @@ files:
176
190
  - MIGRATING_FROM_V3_TO_V4.md
177
191
  - README.md
178
192
  - Rakefile
193
+ - bin/pack
179
194
  - exe/i18n
180
195
  - i18n-js.gemspec
181
196
  - lib/guard/i18n-js.rb
@@ -187,11 +202,19 @@ files:
187
202
  - lib/i18n-js/cli/command.rb
188
203
  - lib/i18n-js/cli/export_command.rb
189
204
  - lib/i18n-js/cli/init_command.rb
205
+ - lib/i18n-js/cli/lint_scripts_command.rb
206
+ - lib/i18n-js/cli/lint_translations_command.rb
207
+ - lib/i18n-js/cli/plugins_command.rb
190
208
  - lib/i18n-js/cli/ui.rb
191
209
  - lib/i18n-js/cli/version_command.rb
210
+ - lib/i18n-js/embed_fallback_translations_plugin.rb
211
+ - lib/i18n-js/lint.js
212
+ - lib/i18n-js/lint.ts
192
213
  - lib/i18n-js/listen.rb
214
+ - lib/i18n-js/plugin.rb
193
215
  - lib/i18n-js/schema.rb
194
216
  - lib/i18n-js/version.rb
217
+ - package.json
195
218
  homepage: https://github.com/fnando/i18n-js
196
219
  licenses:
197
220
  - MIT
@@ -199,10 +222,10 @@ metadata:
199
222
  rubygems_mfa_required: 'true'
200
223
  homepage_uri: https://github.com/fnando/i18n-js
201
224
  bug_tracker_uri: https://github.com/fnando/i18n-js/issues
202
- source_code_uri: https://github.com/fnando/i18n-js/tree/v4.0.1
203
- changelog_uri: https://github.com/fnando/i18n-js/tree/v4.0.1/CHANGELOG.md
204
- documentation_uri: https://github.com/fnando/i18n-js/tree/v4.0.1/README.md
205
- license_uri: https://github.com/fnando/i18n-js/tree/v4.0.1/LICENSE.md
225
+ source_code_uri: https://github.com/fnando/i18n-js/tree/v4.1.0
226
+ changelog_uri: https://github.com/fnando/i18n-js/tree/v4.1.0/CHANGELOG.md
227
+ documentation_uri: https://github.com/fnando/i18n-js/tree/v4.1.0/README.md
228
+ license_uri: https://github.com/fnando/i18n-js/tree/v4.1.0/LICENSE.md
206
229
  post_install_message:
207
230
  rdoc_options: []
208
231
  require_paths:
@@ -218,7 +241,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
241
  - !ruby/object:Gem::Version
219
242
  version: '0'
220
243
  requirements: []
221
- rubygems_version: 3.3.7
244
+ rubygems_version: 3.3.26
222
245
  signing_key:
223
246
  specification_version: 4
224
247
  summary: Export i18n translations and use them on JavaScript.