shimmer 0.0.1 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36dcc714d5d4ddff50af58d02889b9c193754c8a8c8a48b38e989f898805adc5
4
- data.tar.gz: 3a3d6e7b848df467c7aa6ceb2f51071cd325f06da912dbc2926f0f7b7d986193
3
+ metadata.gz: 32060fa00134259da2eb2723aa0ba4e14a95bbd85f9b214797e76778f2c1500b
4
+ data.tar.gz: 18c942b4f19f582e7ae45a89aad5b03d6192201ea0849e14bf9e8f567dccc069
5
5
  SHA512:
6
- metadata.gz: 7716f31b96c8d62da7c94151b59a6006ef12ddd918314aceb3026094cfb414634285e583979e0bc6b0932e16d36e95b69199d0e83c03933148ceaaf3002eac0d
7
- data.tar.gz: 64803d66f26ed93da2265b453493bfe768344273cabb9e0c077678d7c881d73eebdee1701801767adef198da8cda100cc72f5486687c56df2fdb8de865f84dac
6
+ metadata.gz: 752d5f326fcf637ec3fa4d37e3cd6c5a9df88d7c408697ad7c3153b65f75ef1c8fc7f6cbeb8e29d44efe25b314a209a3bf615bd30146f704beb05cd1b156ca52
7
+ data.tar.gz: f8465c8a123e351a84e970fcbb6629e79a0b2759095c6378f248255cb41fdacdbb0dbce46e8751438c0737669d22e2eb8dfe8a9afb3574adfc0d0e031976b716
data/.editorconfig ADDED
@@ -0,0 +1,12 @@
1
+ ; EditorConfig is awesome: http://EditorConfig.org
2
+
3
+ ; top-most EditorConfig file
4
+ root = true
5
+
6
+ ; Unix-style newlines with a newline ending every file
7
+ [*]
8
+ indent_style = spaces
9
+ end_of_line = lf
10
+ insert_final_newline = true
11
+ trim_trailing_whitespace = true
12
+ indent_size = 2
data/.eslintrc.js ADDED
@@ -0,0 +1,25 @@
1
+ module.exports = {
2
+ env: {
3
+ browser: true,
4
+ es6: true,
5
+ },
6
+ parser: "@typescript-eslint/parser",
7
+ extends: [
8
+ "plugin:@typescript-eslint/recommended",
9
+ "plugin:prettier/recommended",
10
+ ],
11
+ parserOptions: {
12
+ ecmaVersion: 2018,
13
+ sourceType: "module",
14
+ },
15
+ plugins: ["@typescript-eslint"],
16
+ rules: {
17
+ "no-console": 2,
18
+ "@typescript-eslint/explicit-function-return-type": [
19
+ "error",
20
+ {
21
+ allowExpressions: true,
22
+ },
23
+ ],
24
+ },
25
+ };
data/.prettierrc ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "trailingComma": "es5"
3
+ }
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.0.3
data/.solargraph.yml ADDED
@@ -0,0 +1,4 @@
1
+ plugins:
2
+ - solargraph-standardrb
3
+ reporters:
4
+ - standardrb
@@ -0,0 +1,3 @@
1
+ {
2
+ "solargraph.useBundler": true
3
+ }
data/Gemfile CHANGED
@@ -6,3 +6,5 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
+ gem "standard"
10
+ gem "solargraph-standardrb"
data/Gemfile.lock ADDED
@@ -0,0 +1,87 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ shimmer (0.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.2)
10
+ backport (1.2.0)
11
+ benchmark (0.2.0)
12
+ diff-lcs (1.4.4)
13
+ e2mmap (0.1.0)
14
+ jaro_winkler (1.5.4)
15
+ kramdown (2.3.1)
16
+ rexml
17
+ kramdown-parser-gfm (1.1.0)
18
+ kramdown (~> 2.0)
19
+ nokogiri (1.12.5-x86_64-darwin)
20
+ racc (~> 1.4)
21
+ nokogiri (1.12.5-x86_64-linux)
22
+ racc (~> 1.4)
23
+ parallel (1.21.0)
24
+ parser (3.0.3.2)
25
+ ast (~> 2.4.1)
26
+ racc (1.6.0)
27
+ rainbow (3.0.0)
28
+ rake (13.0.6)
29
+ regexp_parser (2.2.0)
30
+ reverse_markdown (2.1.1)
31
+ nokogiri
32
+ rexml (3.2.5)
33
+ rubocop (1.23.0)
34
+ parallel (~> 1.10)
35
+ parser (>= 3.0.0.0)
36
+ rainbow (>= 2.2.2, < 4.0)
37
+ regexp_parser (>= 1.8, < 3.0)
38
+ rexml
39
+ rubocop-ast (>= 1.12.0, < 2.0)
40
+ ruby-progressbar (~> 1.7)
41
+ unicode-display_width (>= 1.4.0, < 3.0)
42
+ rubocop-ast (1.15.0)
43
+ parser (>= 3.0.1.1)
44
+ rubocop-performance (1.12.0)
45
+ rubocop (>= 1.7.0, < 2.0)
46
+ rubocop-ast (>= 0.4.0)
47
+ ruby-progressbar (1.11.0)
48
+ solargraph (0.44.2)
49
+ backport (~> 1.2)
50
+ benchmark
51
+ bundler (>= 1.17.2)
52
+ diff-lcs (~> 1.4)
53
+ e2mmap
54
+ jaro_winkler (~> 1.5)
55
+ kramdown (~> 2.3)
56
+ kramdown-parser-gfm (~> 1.1)
57
+ parser (~> 3.0)
58
+ reverse_markdown (>= 1.0.5, < 3)
59
+ rubocop (>= 0.52)
60
+ thor (~> 1.0)
61
+ tilt (~> 2.0)
62
+ yard (~> 0.9, >= 0.9.24)
63
+ solargraph-standardrb (0.0.4)
64
+ solargraph (>= 0.39.1)
65
+ standard (>= 0.4.1)
66
+ standard (1.5.0)
67
+ rubocop (= 1.23.0)
68
+ rubocop-performance (= 1.12.0)
69
+ thor (1.1.0)
70
+ tilt (2.0.10)
71
+ unicode-display_width (2.1.0)
72
+ webrick (1.7.0)
73
+ yard (0.9.27)
74
+ webrick (~> 1.7.0)
75
+
76
+ PLATFORMS
77
+ x86_64-darwin-21
78
+ x86_64-linux
79
+
80
+ DEPENDENCIES
81
+ rake (~> 13.0)
82
+ shimmer!
83
+ solargraph-standardrb
84
+ standard
85
+
86
+ BUNDLED WITH
87
+ 2.2.32
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ class FilesController < ActionController::Base
5
+ def show
6
+ expires_in 1.year, public: true
7
+ request.session_options[:skip] = true # prevents a session cookie from being set (would prevent caching on CDNs)
8
+ proxy = FileProxy.restore(params.require(:id))
9
+ send_data proxy.file,
10
+ filename: proxy.filename.to_s,
11
+ type: proxy.content_type,
12
+ disposition: "inline"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ class SitemapsController < ActionController::Base
5
+ def show
6
+ path = "sitemaps/#{params.require(:path)}.gz"
7
+ send_data ActiveStorage::Blob.service.download(path)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ class SitemapJob < ActiveJob::Base
5
+ def perform
6
+ SitemapGenerator::Interpreter.run
7
+ SitemapGenerator::Sitemap.ping_search_engines
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # https://github.com/rails/rails/issues/22965
4
+ module Shimmer
5
+ class CloudflareProxy
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ return @app.call(env) unless env["HTTP_CF_VISITOR"]
12
+
13
+ env["HTTP_X_FORWARDED_PROTO"] = JSON.parse(env["HTTP_CF_VISITOR"])["scheme"]
14
+ @app.call(env)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ module Shimmer
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ path = File.expand_path(__dir__)
5
+ Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :db do
4
+ desc "Downloads the app database from heroku to local db"
5
+ task pull_data: :environment do
6
+ config = ActiveRecord::Base.connection_db_config.config
7
+ # binding.pry
8
+ ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] = "1"
9
+ Rake::Task["db:drop"].invoke
10
+ ENV["PGUSER"] = config["username"]
11
+ ENV["PGHOST"] = config["host"]
12
+ ENV["PGPORT"] = config["port"].to_s
13
+ sh "heroku pg:pull DATABASE_URL #{config["database"]} --app thefetishtraveller"
14
+ sh "rails db:environment:set"
15
+ sh "RAILS_ENV=test rails db:create"
16
+ end
17
+
18
+ task pull_assets: :environment do
19
+ config = JSON.parse(`heroku config --json`)
20
+ ENV["AWS_DEFAULT_REGION"] = config.fetch("AWS_REGION")
21
+ bucket = config.fetch("AWS_BUCKET")
22
+ ENV["AWS_ACCESS_KEY_ID"] = config.fetch("AWS_ACCESS_KEY_ID")
23
+ ENV["AWS_SECRET_ACCESS_KEY"] = config.fetch("AWS_SECRET_ACCESS_KEY")
24
+ storage_folder = Rails.root.join("storage")
25
+ download_folder = storage_folder.join("downloads")
26
+ FileUtils.mkdir_p download_folder
27
+ sh "aws s3 sync s3://#{bucket} #{storage_folder}/downloads"
28
+ download_folder.each_child do |file|
29
+ next if file.directory?
30
+
31
+ new_path = storage_folder.join file.basename.to_s.then { |e| [e[0..1], e[2..3], e] }.join("/")
32
+ FileUtils.mkdir_p(new_path.dirname)
33
+ FileUtils.cp(file, new_path)
34
+ end
35
+ # purge variants
36
+ ActiveStorage::VariantRecord.delete_all
37
+ ActiveStorage::Blob.update_all(service_name: :local)
38
+ end
39
+
40
+ desc "Download all app data, including assets"
41
+ task pull: [:pull_data, :pull_assets]
42
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc "Executes all linters and tests"
4
+ task :lint do
5
+ sh "bundle exec standardrb --fix"
6
+ sh "yarn lint"
7
+ sh "i18n-tasks health"
8
+ sh "bin/rspec"
9
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module FileHelper
5
+ def image_tag(source, **options)
6
+ return nil if source.blank?
7
+
8
+ if source.is_a?(ActiveStorage::Variant) || source.is_a?(ActiveStorage::Attached) || source.is_a?(ActiveStorage::Attachment) || source.is_a?(ActionText::Attachment)
9
+ attachment = source
10
+ width = options[:width]
11
+ height = options[:height]
12
+ source = image_file_url(source, width: width, height: height)
13
+ options[:loading] = :lazy
14
+ options[:srcset] = "#{source} 1x, #{image_file_url(attachment, width: width.to_i * 2, height: height ? height.to_i * 2 : nil)} 2x" if options[:width].present?
15
+ end
16
+ super source, options
17
+ end
18
+
19
+ def image_file_url(source, width: nil, height: nil)
20
+ return if source.blank?
21
+ return source if source.is_a?(String)
22
+
23
+ blob = source.try(:blob) || source
24
+ Shimmer::FileProxy.new(blob_id: blob.id, width: width, height: height).url
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ class FileProxy
5
+ attr_reader :blob_id, :resize
6
+
7
+ delegate :message_verifier, to: :class
8
+ delegate :content_type, :filename, to: :blob
9
+
10
+ class << self
11
+ def restore(id)
12
+ blob_id, resize = message_verifier.verified(id)
13
+ new blob_id: blob_id, resize: resize
14
+ end
15
+ end
16
+
17
+ def initialize(blob_id:, resize: nil, width: nil, height: nil)
18
+ @blob_id = blob_id
19
+ if !resize && width
20
+ resize = if height
21
+ "#{width}x#{height}>"
22
+ else
23
+ "#{width}x"
24
+ end
25
+ end
26
+ @resize = resize
27
+ end
28
+
29
+ class << self
30
+ def message_verifier
31
+ @message_verifier ||= ApplicationRecord.signed_id_verifier
32
+ end
33
+ end
34
+
35
+ def path
36
+ Rails.application.routes.url_helpers.file_path(id, locale: nil)
37
+ end
38
+
39
+ def url(protocol: Rails.env.production? ? :https : :http)
40
+ Rails.application.routes.url_helpers.file_url(id, locale: nil, protocol: protocol)
41
+ end
42
+
43
+ def blob
44
+ @blob ||= ActiveStorage::Blob.find(blob_id)
45
+ end
46
+
47
+ def resizeable
48
+ resize.present? && !blob.content_type.include?("svg")
49
+ end
50
+
51
+ def variant
52
+ @variantvariant ||= resizeable ? blob.representation(resize: resize).processed : blob
53
+ end
54
+
55
+ def file
56
+ @file ||= blob.service.download(variant.key)
57
+ end
58
+
59
+ private
60
+
61
+ def id
62
+ @id ||= message_verifier.generate([blob_id, resize])
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module Localizable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_action :set_locale
9
+
10
+ def default_url_options(options = {})
11
+ options = {locale: I18n.locale}.merge options
12
+ options[:debug] = true if I18n.debug?
13
+ options
14
+ end
15
+
16
+ def request_locale
17
+ request.env["HTTP_ACCEPT_LANGUAGE"].to_s[0..1].downcase.presence&.then { |e| e if I18n.available_locales.include?(e.to_sym) }
18
+ end
19
+
20
+ def set_locale
21
+ I18n.locale = url_locale || request_locale || I18n.default_locale
22
+ I18n.debug = params.key?(:debug)
23
+ end
24
+
25
+ def check_locale
26
+ redirect_to url_for(locale: I18n.locale) if params[:locale].blank? && request.get? && request.format.html?
27
+ end
28
+
29
+ def url_locale
30
+ params[:locale]
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ module I18n
37
+ UNTRANSLATED_SCOPES = ["number", "transliterate", "date", "datetime", "errors", "helpers", "support", "time", "faker"].map { |k| "#{k}." }
38
+
39
+ thread_mattr_accessor :debug
40
+
41
+ class << self
42
+ alias_method :old_translate, :translate
43
+ def translate(key, options = {})
44
+ key = key.to_s.downcase
45
+ untranslated = UNTRANSLATED_SCOPES.any? { |e| key.include? e }
46
+ key_name = [options[:scope], key].flatten.compact.join(".")
47
+ option_names = options.except(:count, :default, :raise, :scope).map { |k, v| "#{k}=#{v}" }.join(", ")
48
+ return "#{key_name} #{option_names}" if I18n.debug && !untranslated
49
+
50
+ options.reverse_merge!(default: old_translate(key, **options.merge(locale: :de))) if untranslated
51
+ old_translate(key, **options)
52
+ end
53
+ alias_method :t, :translate
54
+
55
+ def debug?
56
+ debug
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ module RemoteNavigation
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ def ui
9
+ @ui ||= RemoteNavigator.new turbo_stream: turbo_stream
10
+ end
11
+
12
+ def default_render
13
+ return render_modal if shimmer_request?
14
+ return super unless ui.updates?
15
+
16
+ render turbo_stream: ui.queued_updates.join("\n")
17
+ end
18
+
19
+ helper_method :modal_path
20
+ def modal_path(url, id: nil, size: nil, close: false)
21
+ "javascript:ui.modal.open(#{{url: url, id: id, size: size, close: close}.to_json})"
22
+ end
23
+
24
+ helper_method :close_modal_path
25
+ def close_modal_path(id: nil)
26
+ "javascript:ui.modal.close(#{{id: id}.to_json})"
27
+ end
28
+
29
+ def shimmer_request?
30
+ request.headers["X-Shimmer"].present?
31
+ end
32
+
33
+ def render_modal
34
+ enforce_modal
35
+ render layout: false
36
+ end
37
+
38
+ def enforce_modal
39
+ raise "trying to render a modal from a regular request" unless shimmer_request?
40
+ end
41
+ end
42
+ end
43
+
44
+ class RemoteNavigator
45
+ attr_reader :turbo_stream
46
+
47
+ def initialize(turbo_stream:)
48
+ @turbo_stream = turbo_stream
49
+ end
50
+
51
+ def queued_updates
52
+ @queued_updates ||= []
53
+ end
54
+
55
+ def updates?
56
+ queued_updates.any?
57
+ end
58
+
59
+ def run_javascript(script)
60
+ queued_updates.push turbo_stream.append "shimmer", "<div class='hidden' data-controller='remote-navigation'>#{script}</div>"
61
+ end
62
+
63
+ def replace(id, with: id, **locals)
64
+ queued_updates.push turbo_stream.replace(id, partial: with, locals: locals)
65
+ end
66
+
67
+ def prepend(id, with:, **locals)
68
+ queued_updates.push turbo_stream.prepend(id, partial: with, locals: locals)
69
+ end
70
+
71
+ def append(id, with:, **locals)
72
+ queued_updates.push turbo_stream.append(id, partial: with, locals: locals)
73
+ end
74
+
75
+ def remove(id)
76
+ queued_updates.push turbo_stream.remove(id)
77
+ end
78
+
79
+ def open_modal(path)
80
+ run_javascript "ui.modal.open('#{path}')"
81
+ end
82
+
83
+ def close_modal
84
+ run_javascript "ui.modal.close()"
85
+ end
86
+
87
+ def navigate_to(path)
88
+ close_modal
89
+ path = polymorphic_path(path) unless path.is_a?(String)
90
+ run_javascript "Turbo.visit('#{path}')"
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shimmer
4
+ class SitemapAdapter
5
+ def write(location, raw_data)
6
+ SitemapGenerator::FileAdapter.new.write(location, raw_data)
7
+ ActiveStorage::Blob.service.upload("sitemaps/#{location.path_in_public}", File.open(location.path))
8
+ end
9
+ end
10
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shimmer
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.4"
5
5
  end
data/lib/shimmer.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "shimmer/version"
4
+ require_relative "shimmer/railtie" if defined?(Rails::Railtie)
5
+ Dir["#{File.expand_path("../lib/shimmer/middlewares", __dir__)}/*"].each { |e| require e }
6
+ Dir["#{File.expand_path("../lib/shimmer/controllers", __dir__)}/*"].each { |e| require e }
7
+ Dir["#{File.expand_path("../lib/shimmer/jobs", __dir__)}/*"].each { |e| require e }
8
+ Dir["#{File.expand_path("../lib/shimmer/utils", __dir__)}/*"].each { |e| require e }
4
9
 
5
10
  module Shimmer
6
11
  class Error < StandardError; end
data/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@nerdgeschoss/shimmer",
3
+ "version": "0.0.1",
4
+ "description": "Simple application development in Rails",
5
+ "main": "dist/index.cjs.js",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "yarn build:js && yarn build:types",
13
+ "build:js": "NODE_ENV=production rollup -c",
14
+ "build:types": "tsc --emitDeclarationOnly",
15
+ "format": "prettier --write \"src/**/*.{ts,css,scss,json,yml}\"",
16
+ "lint": "yarn lint:types && yarn lint:style",
17
+ "lint:types": "tsc --noEmit",
18
+ "lint:style": "eslint src/**/*.ts --max-warnings 0"
19
+ },
20
+ "contributors": [
21
+ "Jens Ravens"
22
+ ],
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "@rails/request.js": "^0.0.6"
26
+ },
27
+ "devDependencies": {
28
+ "@typescript-eslint/eslint-plugin": "^5.6.0",
29
+ "@typescript-eslint/parser": "^5.6.0",
30
+ "esbuild": "^0.14.2",
31
+ "eslint": "^8.4.1",
32
+ "eslint-config-prettier": "^8.3.0",
33
+ "eslint-plugin-prettier": "^4.0.0",
34
+ "prettier": "^2.5.1",
35
+ "rollup": "^2.61.0",
36
+ "rollup-plugin-cleaner": "^1.0.0",
37
+ "rollup-plugin-esbuild": "^4.7.2",
38
+ "typescript": "^4.1.3"
39
+ },
40
+ "keywords": [
41
+ "rails",
42
+ "form",
43
+ "modal"
44
+ ],
45
+ "bugs": {
46
+ "url": "https://github.com/nerdgeschoss/shimmer/issues"
47
+ },
48
+ "homepage": "https://github.com/nerdgeschoss/shimmer#readme"
49
+ }
data/rollup.config.js ADDED
@@ -0,0 +1,26 @@
1
+ import esbuild from "rollup-plugin-esbuild";
2
+ import cleaner from "rollup-plugin-cleaner";
3
+ import pkg from "./package.json";
4
+
5
+ export default {
6
+ input: "./src/index.ts",
7
+ external: ["@rails/request.js"],
8
+ plugins: [
9
+ cleaner({
10
+ targets: ["./dist/"],
11
+ }),
12
+ esbuild({
13
+ target: "es6",
14
+ }),
15
+ ],
16
+ output: [
17
+ {
18
+ file: pkg.main,
19
+ format: "cjs",
20
+ },
21
+ {
22
+ file: pkg.module,
23
+ format: "es",
24
+ },
25
+ ],
26
+ };
data/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ declare global {
2
+ interface Window {
3
+ ui: typeof ui;
4
+ }
5
+ }
6
+
7
+ import { ModalPresenter } from "./modal";
8
+ import "./touch";
9
+
10
+ export const ui = {
11
+ modal: new ModalPresenter(),
12
+ };
13
+
14
+ window.ui = ui;
15
+
16
+ export { registerServiceWorker } from "./serviceworker";
17
+ export { currentLocale } from "./locale";
data/src/locale.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function currentLocale(): string {
2
+ return (document.documentElement.lang || "en") as string;
3
+ }