From 0d4219d6c57516e12f2a09b7637a3afa2e4434e5 Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Fri, 11 Nov 2022 18:19:36 -0500 Subject: [PATCH 01/15] beginnings of i18n support --- Gemfile.lock | 2 + example/Gemfile.lock | 2 + example/app/pages/demos/i18n/page.haml | 27 +++++++-- .../{page.en-US.toml => page.intl.en-US.toml} | 0 lib/mayu/resources/resource.rb | 3 + lib/mayu/resources/types.rb | 3 + lib/mayu/resources/types/component.rb | 14 +++++ lib/mayu/resources/types/translations.rb | 56 +++++++++++++++++++ mayu-live.gemspec | 1 + 9 files changed, 103 insertions(+), 5 deletions(-) rename example/app/pages/demos/i18n/{page.en-US.toml => page.intl.en-US.toml} (100%) create mode 100644 lib/mayu/resources/types/translations.rb diff --git a/Gemfile.lock b/Gemfile.lock index 2464863b..22209347 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,6 +2,7 @@ PATH remote: . specs: mayu-live (0.0.0) + accept_language (~> 2.0.3) async (~> 2.0.3) async-container (~> 0.16.12) async-http (~> 0.59.1) @@ -32,6 +33,7 @@ PATH GEM remote: https://rubygems.org/ specs: + accept_language (2.0.3) ansi (1.5.0) ast (2.4.2) async (2.0.3) diff --git a/example/Gemfile.lock b/example/Gemfile.lock index fb698ce4..48cf341e 100644 --- a/example/Gemfile.lock +++ b/example/Gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: mayu-live (0.0.0) + accept_language (~> 2.0.3) async (~> 2.0.3) async-container (~> 0.16.12) async-http (~> 0.59.1) @@ -32,6 +33,7 @@ PATH GEM remote: https://rubygems.org/ specs: + accept_language (2.0.3) async (2.0.3) console (~> 1.10) io-event (~> 1.0.0) diff --git a/example/app/pages/demos/i18n/page.haml b/example/app/pages/demos/i18n/page.haml index f30b4ae6..1bcf0c34 100644 --- a/example/app/pages/demos/i18n/page.haml +++ b/example/app/pages/demos/i18n/page.haml @@ -1,10 +1,27 @@ :ruby Heading = import("/app/components/Layout/Heading") + T = translations("sv-SE", "en-US") - def t - "TODO: Implement me" + def self.get_initial_state(**) = { + lang: "en-US" + } + + def t(*path) + T.fetch(lang).dig(*path) || "Translation not found" + end + + def handle_set_lang(e) + e => { target: { value: lang } } + update(lang:) end -%article - %Heading(level=2)= t(:title) - %slot + def lang = state[:lang] + +%article(lang=lang) + %Heading(level=1)= t(:title) + + %p= t(:body) + + %div + = T.keys.map do |value| + %button(onclick=handle_set_lang value=value)= value diff --git a/example/app/pages/demos/i18n/page.en-US.toml b/example/app/pages/demos/i18n/page.intl.en-US.toml similarity index 100% rename from example/app/pages/demos/i18n/page.en-US.toml rename to example/app/pages/demos/i18n/page.intl.en-US.toml diff --git a/lib/mayu/resources/resource.rb b/lib/mayu/resources/resource.rb index a110d3fc..4d1ea67d 100644 --- a/lib/mayu/resources/resource.rb +++ b/lib/mayu/resources/resource.rb @@ -94,6 +94,9 @@ def generate_assets(assets_dir) end end + sig { returns(String) } + def basename_without_extension = File.basename(@path, ".*") + sig { returns(String) } def app_root = @registry.root diff --git a/lib/mayu/resources/types.rb b/lib/mayu/resources/types.rb index 069ac4d9..8bd693ec 100644 --- a/lib/mayu/resources/types.rb +++ b/lib/mayu/resources/types.rb @@ -6,6 +6,7 @@ require_relative "types/image" require_relative "types/stylesheet" require_relative "types/javascript" +require_relative "types/translations" require_relative "types/svg" module Mayu @@ -22,6 +23,8 @@ def self.for_path(path) return Component when /\.js\z/ return JavaScript + when Translations::FILENAME_RE + return Translations when /\.css\z/ return Stylesheet when /\.(png|jpe?g|gif|webp)$\z/ diff --git a/lib/mayu/resources/types/component.rb b/lib/mayu/resources/types/component.rb index 50c6e014..e42b9175 100644 --- a/lib/mayu/resources/types/component.rb +++ b/lib/mayu/resources/types/component.rb @@ -36,6 +36,20 @@ def self.svg(path) __resource.import(path) => SVG => impl impl end + + sig do + params(locales: String).returns( + T::Hash[String, T::Hash[Symbol, T.untyped]] + ) + end + def self.translations(*locales) + locales.each_with_object({}) do |locale, hash| + __resource.import( + "./#{__resource.basename_without_extension}.intl.#{locale}.toml" + ) => Translations => impl + hash[locale] = impl.to_h + end + end end end end diff --git a/lib/mayu/resources/types/translations.rb b/lib/mayu/resources/types/translations.rb new file mode 100644 index 00000000..5c30832e --- /dev/null +++ b/lib/mayu/resources/types/translations.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +# typed: strict + +require "toml-rb" +require_relative "base" + +module Mayu + module Resources + module Types + class Translations < Base + extend T::Sig + + FILENAME_RE = /\.intl\.(.+)\.toml\z/ + + sig { params(resource: Resource).void } + def initialize(resource) + @resource = resource + + @locale_identifier = + T.let(File.basename(resource.path)[FILENAME_RE, 1].to_s, String) + + @translations = + T.let( + TomlRB.parse( + resource.read(encoding: "utf-8"), + symbolize_keys: true + ), + T::Hash[Symbol, T.untyped] + ) + end + + sig { returns(T::Array[Asset]) } + def assets + [] + end + + sig { returns(T::Hash[Symbol, T.untyped]) } + def to_h + @translations + end + + MarshalFormat = T.type_alias { [String, T::Hash[Symbol, T.untyped]] } + + sig { returns(MarshalFormat) } + def marshal_dump + [@locale_identifier, @translations] + end + + sig { params(args: MarshalFormat).void } + def marshal_load(args) + @locale_identifier, @translations = args + end + end + end + end +end diff --git a/mayu-live.gemspec b/mayu-live.gemspec index 0abbffbd..0d23a4ff 100644 --- a/mayu-live.gemspec +++ b/mayu-live.gemspec @@ -36,6 +36,7 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] + spec.add_dependency "accept_language", "~> 2.0.3" spec.add_dependency "async", "~> 2.0.3" spec.add_dependency "async-container", "~> 0.16.12" spec.add_dependency "async-http", "~> 0.59.1" From 2bc57633ee2185fac2b4e56f6954becf632a82db Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Fri, 11 Nov 2022 20:49:55 -0500 Subject: [PATCH 02/15] i18n: Respect the accept-language header Next step is to add a preferred-language cookie --- example/app/pages/demos/i18n/page.haml | 23 ++- .../app/pages/demos/i18n/page.intl.en-US.toml | 1 + .../app/pages/demos/i18n/page.intl.sv-SE.toml | 1 + lib/mayu/component/helpers.rb | 5 + lib/mayu/server/errors.rb | 1 + lib/mayu/session.rb | 8 ++ lib/mayu/vdom/vnode.rb | 5 + lib/mayu/vdom/vtree.rb | 5 + sorbet/rbi/gems/accept_language@2.0.3.rbi | 132 ++++++++++++++++++ sorbet/tapioca/require.rb | 1 + 10 files changed, 167 insertions(+), 15 deletions(-) create mode 100644 sorbet/rbi/gems/accept_language@2.0.3.rbi diff --git a/example/app/pages/demos/i18n/page.haml b/example/app/pages/demos/i18n/page.haml index 1bcf0c34..0d1bb79f 100644 --- a/example/app/pages/demos/i18n/page.haml +++ b/example/app/pages/demos/i18n/page.haml @@ -2,26 +2,19 @@ Heading = import("/app/components/Layout/Heading") T = translations("sv-SE", "en-US") - def self.get_initial_state(**) = { - lang: "en-US" - } - - def t(*path) - T.fetch(lang).dig(*path) || "Translation not found" + def lang + helpers.get_accepted_language(T.keys) end - def handle_set_lang(e) - e => { target: { value: lang } } - update(lang:) + def t(*path, **replacements) + value = T.fetch(lang) { raise "No translations!" }.dig(*path) + return "Missing translation for #{lang} at #{path.join(".")}!" unless value + p(lang:, value:, replacements:) + format(value, replacements) end - def lang = state[:lang] - %article(lang=lang) %Heading(level=1)= t(:title) %p= t(:body) - - %div - = T.keys.map do |value| - %button(onclick=handle_set_lang value=value)= value + %p= t(:current_language, lang:) diff --git a/example/app/pages/demos/i18n/page.intl.en-US.toml b/example/app/pages/demos/i18n/page.intl.en-US.toml index a0397cb8..796b790b 100644 --- a/example/app/pages/demos/i18n/page.intl.en-US.toml +++ b/example/app/pages/demos/i18n/page.intl.en-US.toml @@ -2,3 +2,4 @@ title = "Internationalization" body = """ hello world """ +current_language = "Current language: %s" diff --git a/example/app/pages/demos/i18n/page.intl.sv-SE.toml b/example/app/pages/demos/i18n/page.intl.sv-SE.toml index 50f9d3be..3665dad6 100644 --- a/example/app/pages/demos/i18n/page.intl.sv-SE.toml +++ b/example/app/pages/demos/i18n/page.intl.sv-SE.toml @@ -2,3 +2,4 @@ title = "Internationalisering" body = """ hej världen """ +current_language = "Nuvarande språk: %s" diff --git a/lib/mayu/component/helpers.rb b/lib/mayu/component/helpers.rb index a97d7731..8ca6e470 100644 --- a/lib/mayu/component/helpers.rb +++ b/lib/mayu/component/helpers.rb @@ -37,6 +37,11 @@ def alert(message) @vnode.action(:alert, message) end + sig { params(languages: T::Array[String]).returns(T.nilable(String)) } + def get_accepted_language(languages) + @vnode.get_accepted_language(languages) || languages.first + end + # sig { returns(Mayu::State::Store) } # def store # @vnode.store diff --git a/lib/mayu/server/errors.rb b/lib/mayu/server/errors.rb index 92c037cc..234aeff9 100644 --- a/lib/mayu/server/errors.rb +++ b/lib/mayu/server/errors.rb @@ -45,6 +45,7 @@ def self.handle_exceptions(&block) def self.log_exceptions(&block) yield rescue => e + Console.logger.error(self, e) Console.logger.error(self, "#{e.class.name}: #{e.message}") raise end diff --git a/lib/mayu/session.rb b/lib/mayu/session.rb index 848a07ab..416adb47 100644 --- a/lib/mayu/session.rb +++ b/lib/mayu/session.rb @@ -2,6 +2,7 @@ require "time" require "nanoid" +require "accept_language" require_relative "environment" require_relative "vdom/vtree" require_relative "vdom/marshalling" @@ -136,6 +137,13 @@ def initialize(environment:, path:, headers: {}, vtree: nil, store: nil) @app = T.let(environment.load_root(path, headers:), VDOM::Descriptor) @last_ping_at = T.let(Time.now.to_f, Float) @barrier = T.let(Async::Barrier.new, Async::Barrier) + @accept_language = T.let(nil, T.nilable(AcceptLanguage::Parser)) + end + + sig { returns(AcceptLanguage::Parser) } + def accept_language + @accept_language ||= + AcceptLanguage.parse(@headers["accept-language"].to_s) end sig { void } diff --git a/lib/mayu/vdom/vnode.rb b/lib/mayu/vdom/vnode.rb index 19ea6973..33e3af71 100644 --- a/lib/mayu/vdom/vnode.rb +++ b/lib/mayu/vdom/vnode.rb @@ -140,6 +140,11 @@ def action(type, payload) @vtree.action(type, payload) end + sig { params(languages: T::Array[String]).returns(T.nilable(String)) } + def get_accepted_language(languages) + @vtree.get_accepted_language(languages) + end + sig { void } def enqueue_update! @vtree.enqueue_update!(self) diff --git a/lib/mayu/vdom/vtree.rb b/lib/mayu/vdom/vtree.rb index 53fc5556..49fe3e42 100644 --- a/lib/mayu/vdom/vtree.rb +++ b/lib/mayu/vdom/vtree.rb @@ -185,6 +185,11 @@ def initialize(session:, task: Async::Task.current) @asset_refs = T.let(RefCounter.new, RefCounter[String]) end + sig { params(languages: T::Array[String]).returns(T.nilable(String)) } + def get_accepted_language(languages) + T.unsafe(@session.accept_language).match(*languages) + end + sig { returns(T::Array[T.untyped]) } def marshal_dump [@root, @id_generator, @sent_stylesheets] diff --git a/sorbet/rbi/gems/accept_language@2.0.3.rbi b/sorbet/rbi/gems/accept_language@2.0.3.rbi new file mode 100644 index 00000000..4200ae05 --- /dev/null +++ b/sorbet/rbi/gems/accept_language@2.0.3.rbi @@ -0,0 +1,132 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for types exported from the `accept_language` gem. +# Please instead update this file by running `bin/tapioca gem accept_language`. + +# Tiny library for parsing the Accept-Language header. +# +# @example +# AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7") # => #0.1e1, "en-GB"=>0.8e0, "en"=>0.7e0}> +# @see https://tools.ietf.org/html/rfc2616#section-14.4 +# +# source://accept_language//lib/accept_language.rb#7 +module AcceptLanguage + class << self + # @example + # parse("da, en-GB;q=0.8, en;q=0.7") # => #0.1e1, "en-GB"=>0.8e0, "en"=>0.7e0}> + # @note Parse an Accept-Language header field into a language range. + # @return [#match] a parser that responds to #match. + # + # source://accept_language//lib/accept_language.rb#12 + def parse(field); end + end +end + +# @example +# Matcher.new("da" => 1.0, "en-GB" => 0.8, "en" => 0.7).call(:ug, :kk, :ru, :en) # => :en +# Matcher.new("da" => 1.0, "en-GB" => 0.8, "en" => 0.7).call(:fr, :en, :"en-GB") # => :"en-GB" +# @note Compare an Accept-Language header value with your application's +# supported languages to find the common languages that could be presented +# to a user. +# +# source://accept_language//lib/accept_language/matcher.rb#10 +class AcceptLanguage::Matcher + # @param languages_range [Hash] A list of accepted + # languages with their respective qualities. + # @return [Matcher] a new instance of Matcher + # + # source://accept_language//lib/accept_language/matcher.rb#17 + def initialize(**languages_range); end + + # @example Uyghur, Kazakh, Russian and English languages are available. + # call(:ug, :kk, :ru, :en) + # @param available_langtags [Array] The list of available + # languages. + # @return [String, Symbol, nil] The language that best matches. + # + # source://accept_language//lib/accept_language/matcher.rb#38 + def call(*available_langtags); end + + # Returns the value of attribute excluded_langtags. + # + # source://accept_language//lib/accept_language/matcher.rb#13 + def excluded_langtags; end + + # Returns the value of attribute preferred_langtags. + # + # source://accept_language//lib/accept_language/matcher.rb#13 + def preferred_langtags; end + + private + + # source://accept_language//lib/accept_language/matcher.rb#57 + def any_other_langtag(*available_langtags); end + + # source://accept_language//lib/accept_language/matcher.rb#67 + def drop_unacceptable(*available_langtags); end + + # @return [Boolean] + # + # source://accept_language//lib/accept_language/matcher.rb#75 + def unacceptable?(langtag); end + + # @return [Boolean] + # + # source://accept_language//lib/accept_language/matcher.rb#81 + def wildcard?(value); end +end + +# source://accept_language//lib/accept_language/matcher.rb#11 +AcceptLanguage::Matcher::WILDCARD = T.let(T.unsafe(nil), String) + +# @example +# Parser.new("da, en-GB;q=0.8, en;q=0.7") # => #0.1e1, "en-GB"=>0.8e0, "en"=>0.7e0}> +# @note Parser for Accept-Language header fields. +# @see https://tools.ietf.org/html/rfc2616#section-14.4 +# +# source://accept_language//lib/accept_language/parser.rb#10 +class AcceptLanguage::Parser + # @param field [String] The Accept-Language header field to parse. + # @return [Parser] a new instance of Parser + # @see https://tools.ietf.org/html/rfc2616#section-14.4 + # + # source://accept_language//lib/accept_language/parser.rb#20 + def initialize(field); end + + # Returns the value of attribute languages_range. + # + # source://accept_language//lib/accept_language/parser.rb#16 + def languages_range; end + + # @example Uyghur, Kazakh, Russian and English languages are available. + # match(:ug, :kk, :ru, :en) + # @param available_langtags [Array] The list of available + # languages. + # @return [String, Symbol, nil] The language that best matches. + # + # source://accept_language//lib/accept_language/parser.rb#29 + def match(*available_langtags); end + + private + + # @example + # import('da, en-GB;q=0.8, en;q=0.7') # => {"da"=>0.1e1, "en-GB"=>0.8e0, "en"=>0.7e0} + # @return [Hash] A list of accepted languages with their + # respective qualities. + # + # source://accept_language//lib/accept_language/parser.rb#39 + def import(field); end +end + +# source://accept_language//lib/accept_language/parser.rb#11 +AcceptLanguage::Parser::DEFAULT_QUALITY = T.let(T.unsafe(nil), BigDecimal) + +# source://accept_language//lib/accept_language/parser.rb#12 +AcceptLanguage::Parser::SEPARATOR = T.let(T.unsafe(nil), String) + +# source://accept_language//lib/accept_language/parser.rb#13 +AcceptLanguage::Parser::SPACE = T.let(T.unsafe(nil), String) + +# source://accept_language//lib/accept_language/parser.rb#14 +AcceptLanguage::Parser::SUFFIX = T.let(T.unsafe(nil), String) diff --git a/sorbet/tapioca/require.rb b/sorbet/tapioca/require.rb index ddfdbe3a..8294545c 100644 --- a/sorbet/tapioca/require.rb +++ b/sorbet/tapioca/require.rb @@ -3,6 +3,7 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "vendor", "patches")) +require "accept_language" require "async/barrier" require "async/clock" require "async/container" From f70e1609cadf296599cc9cca15001b1d8a522585 Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Fri, 11 Nov 2022 22:00:27 -0500 Subject: [PATCH 03/15] i18n: progress, only set prefered language cookie when value has changed --- example/app/pages/demos/i18n/page.haml | 9 +++- example/app/pages/demos/layout.haml | 1 + lib/mayu/client/src/main.ts | 3 ++ lib/mayu/component/helpers.rb | 5 ++ lib/mayu/server/app.rb | 65 ++++++++++++++++++++------ lib/mayu/session.rb | 39 ++++++++++++++-- lib/mayu/vdom/vnode.rb | 5 ++ lib/mayu/vdom/vtree.rb | 8 ++++ 8 files changed, 118 insertions(+), 17 deletions(-) diff --git a/example/app/pages/demos/i18n/page.haml b/example/app/pages/demos/i18n/page.haml index 0d1bb79f..35a958f2 100644 --- a/example/app/pages/demos/i18n/page.haml +++ b/example/app/pages/demos/i18n/page.haml @@ -9,12 +9,19 @@ def t(*path, **replacements) value = T.fetch(lang) { raise "No translations!" }.dig(*path) return "Missing translation for #{lang} at #{path.join(".")}!" unless value - p(lang:, value:, replacements:) format(value, replacements) end + def handle_set_language(e) + e => { target: { value: } } + helpers.set_prefer_language(value) + end + %article(lang=lang) %Heading(level=1)= t(:title) %p= t(:body) %p= t(:current_language, lang:) + %div + = T.keys.map do |language| + %button(onclick=handle_set_language value=language)= language diff --git a/example/app/pages/demos/layout.haml b/example/app/pages/demos/layout.haml index 10a988ae..bf7ebd0a 100644 --- a/example/app/pages/demos/layout.haml +++ b/example/app/pages/demos/layout.haml @@ -6,6 +6,7 @@ LINKS = { "/demos" => "Demos", + "/demos/i18n" => "I18n", "/demos/pokemon" => "Pokémon", "/demos/tree" => "App tree", "/demos/form" => "Form elements", diff --git a/lib/mayu/client/src/main.ts b/lib/mayu/client/src/main.ts index 67e07515..ab1326f0 100644 --- a/lib/mayu/client/src/main.ts +++ b/lib/mayu/client/src/main.ts @@ -208,6 +208,9 @@ async function main(url: string) { case "session.exception": showException(payload); break; + case "session.set_prefer_language": + logger.info("Changed preferred language:", payload); + break; case "ping": const values = Object.values(payload.values) as number[]; const mean = values.reduce((a, b) => a + b, 0.0) / values.length; diff --git a/lib/mayu/component/helpers.rb b/lib/mayu/component/helpers.rb index 8ca6e470..cdf05fa0 100644 --- a/lib/mayu/component/helpers.rb +++ b/lib/mayu/component/helpers.rb @@ -42,6 +42,11 @@ def get_accepted_language(languages) @vnode.get_accepted_language(languages) || languages.first end + sig { params(language: String).void } + def set_prefer_language(language) + @vnode.set_prefer_language(language) + end + # sig { returns(Mayu::State::Store) } # def store # @vnode.store diff --git a/lib/mayu/server/app.rb b/lib/mayu/server/app.rb index 5d0d5d1a..3eb620aa 100644 --- a/lib/mayu/server/app.rb +++ b/lib/mayu/server/app.rb @@ -16,6 +16,9 @@ class App PING_INTERVAL = 2 # seconds NANOID_RE = /[\w-]{21}/ + PREFER_LANGUAGE_COOKIE = "mayu-prefer-language" + TOKEN_COOKIE = "mayu-token" + MIME_TYPES = T.let( { @@ -256,6 +259,7 @@ def handle_session_post(request, session_id, path) "content-type": "application/json", "set-cookie": set_token_cookie_value(session) } + session.log.push( :pong, pong: ping, @@ -270,11 +274,23 @@ def handle_session_post(request, session_id, path) session.handle_callback("navigate", { path: }) Protocol::HTTP::Response[200, headers, ["ok"]] in ["callback", String => callback_id] + prefer_language = session.prefer_language + session.handle_callback( callback_id, JSON.parse(request.read, symbolize_names: true) ) - headers = { "set-cookie": set_token_cookie_value(session) } + + headers = Protocol::HTTP::Headers.new + + unless session.prefer_language == prefer_language + headers.add( + "set-cookie", + set_prefer_language_cookie_value(session.prefer_language.to_s) + ) + end + + headers.add("set-cookie", set_token_cookie_value(session)) Protocol::HTTP::Response[200, headers, ["ok"]] end end @@ -305,11 +321,14 @@ def handle_session_init(request) "Expected sec-fetch-dest to equal document but got #{value.inspect}" end + cookies = get_cookies(request) + session = Session.new( environment: @environment, path: request.path, - headers: request.headers.to_h.freeze + headers: request.headers.to_h.freeze, + prefer_language: cookies[PREFER_LANGUAGE_COOKIE] ) body = Async::HTTP::Body::Writable.new @@ -465,25 +484,45 @@ def perform_transfer(session, stream, task: Async::Task.current) stream.close end - sig { params(request: Protocol::HTTP::Request).returns(String) } - def get_token_cookie_value(request) - Array(request.headers["cookie"]).each do |str| - if match = str.match(/^mayu-token=(\w+)/) - return match[1].to_s.tap { Session.validate_token!(_1) } - end + sig do + params(request: Protocol::HTTP::Request).returns( + T::Hash[String, String] + ) + end + def get_cookies(request) + Array(request.headers["cookie"]).each_with_object({}) do |str, obj| + key, value = str.split("=", 2) + obj[key] = value end + end + + sig { params(language: String).returns(String) } + def set_prefer_language_cookie_value(language) + return "#{PREFER_LANGUAGE_COOKIE}=; max-age=0" if language.empty? - raise Errors::CookieNotSet + [ + "#{PREFER_LANGUAGE_COOKIE}=#{language}", + "expires=", + "path=/", + "secure", + "SameSite=Strict" + ].join("; ") + end + + sig { params(request: Protocol::HTTP::Request).returns(String) } + def get_token_cookie_value(request) + get_cookies(request) + .fetch(TOKEN_COOKIE) { raise Errors::CookieNotSet } + .to_s + .tap { Session.validate_token!(_1) } end sig { params(session: Session, ttl_seconds: Integer).returns(String) } def set_token_cookie_value(session, ttl_seconds: 60) - expires = Time.now.utc + ttl_seconds - [ - "mayu-token=#{session.token}", + "#{TOKEN_COOKIE}=#{session.token}", "path=/__mayu/session/#{session.id}/", - "expires=#{expires.httpdate}", + "max-age=#{ttl_seconds}", "secure", "HttpOnly", "SameSite=Strict" diff --git a/lib/mayu/session.rb b/lib/mayu/session.rb index 416adb47..f0239c1a 100644 --- a/lib/mayu/session.rb +++ b/lib/mayu/session.rb @@ -93,6 +93,8 @@ def authorized?(token) attr_reader :token sig { returns(String) } attr_reader :path + sig { returns(T.nilable(String)) } + attr_reader :prefer_language sig { returns(T::Hash[String, String]) } attr_reader :headers sig { returns(Environment) } @@ -102,6 +104,12 @@ def authorized?(token) sig { returns(EventStream::Log) } attr_reader :log + sig { params(language: String).returns(String) } + def prefer_language=(language) + @accept_language = nil + @prefer_language = language + end + sig { params(timeout_seconds: T.any(Float, Integer)).returns(T::Boolean) } def expired?(timeout_seconds = 30) seconds_since_last_ping > timeout_seconds @@ -117,11 +125,19 @@ def seconds_since_last_ping environment: Environment, path: String, headers: T::Hash[String, String], + prefer_language: T.nilable(String), vtree: T.nilable(VDOM::VTree), store: T.nilable(State::Store) ).void end - def initialize(environment:, path:, headers: {}, vtree: nil, store: nil) + def initialize( + environment:, + path:, + headers: {}, + prefer_language: nil, + vtree: nil, + store: nil + ) @environment = environment @id = T.let(Nanoid.generate, String) @token = T.let(self.class.generate_token, String) @@ -137,13 +153,26 @@ def initialize(environment:, path:, headers: {}, vtree: nil, store: nil) @app = T.let(environment.load_root(path, headers:), VDOM::Descriptor) @last_ping_at = T.let(Time.now.to_f, Float) @barrier = T.let(Async::Barrier.new, Async::Barrier) + @prefer_language = prefer_language @accept_language = T.let(nil, T.nilable(AcceptLanguage::Parser)) end sig { returns(AcceptLanguage::Parser) } def accept_language @accept_language ||= - AcceptLanguage.parse(@headers["accept-language"].to_s) + begin + accept_language = + AcceptLanguage.parse(@headers["accept-language"].to_s) + + if @prefer_language + accept_language.instance_variable_get(:@languages_range).store( + @prefer_language, + BigDecimal(2) + ) + end + + accept_language + end end sig { void } @@ -239,6 +268,7 @@ def marshal_dump @token, @path, @headers, + @prefer_language, VDOM::Marshalling.dump(@vtree), Marshal.dump(@store.state) ] @@ -246,7 +276,7 @@ def marshal_dump sig { params(a: T::Array[T.untyped]).void } def marshal_load(a) - @id, @token, @path, @headers, dumped_vtree, state = a + @id, @token, @path, @headers, @prefer_language, dumped_vtree, state = a @last_ping_at = Time.now.to_f @vtree = VDOM::Marshalling.restore(dumped_vtree, session: self) @store = @environment.create_store(initial_state: Marshal.restore(state)) @@ -338,6 +368,9 @@ def run(task: Async::Task.current, &block) yield [:navigate, path: href.force_encoding("utf-8")] in [:action, payload] yield [:action, payload] + in [:set_prefer_language, language] + self.prefer_language = language + yield [:set_prefer_language, language] in [:update_finished, *] # noop else diff --git a/lib/mayu/vdom/vnode.rb b/lib/mayu/vdom/vnode.rb index 33e3af71..420435a5 100644 --- a/lib/mayu/vdom/vnode.rb +++ b/lib/mayu/vdom/vnode.rb @@ -145,6 +145,11 @@ def get_accepted_language(languages) @vtree.get_accepted_language(languages) end + sig { params(language: String).void } + def set_prefer_language(language) + @vtree.set_prefer_language(language) + end + sig { void } def enqueue_update! @vtree.enqueue_update!(self) diff --git a/lib/mayu/vdom/vtree.rb b/lib/mayu/vdom/vtree.rb index 49fe3e42..54dcb64a 100644 --- a/lib/mayu/vdom/vtree.rb +++ b/lib/mayu/vdom/vtree.rb @@ -111,6 +111,8 @@ def update(assets: T::Set[String].new, metrics: nil, &block) yield [:action, payload] in [:exception, error] yield [:exception, error] + in [:set_prefer_language, language] + yield [:set_prefer_language, language] in [:pong, timestamp] yield [:pong, timestamp] in VNode => vnode @@ -190,6 +192,12 @@ def get_accepted_language(languages) T.unsafe(@session.accept_language).match(*languages) end + sig { params(language: String).void } + def set_prefer_language(language) + @session.prefer_language = language + @update_queue.enqueue([:set_prefer_language, language]) + end + sig { returns(T::Array[T.untyped]) } def marshal_dump [@root, @id_generator, @sent_stylesheets] From bc9c5a4a257f74e9baadae9da97de0297f99cd5e Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Fri, 11 Nov 2022 22:45:27 -0500 Subject: [PATCH 04/15] i18n: move some i18n helpers into component --- example/app/pages/demos/i18n/page.haml | 13 ++---------- lib/mayu/component/base.rb | 28 ++++++++++++++++++++++++++ lib/mayu/resources/types/component.rb | 23 +++++++++++---------- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/example/app/pages/demos/i18n/page.haml b/example/app/pages/demos/i18n/page.haml index 35a958f2..77d4a088 100644 --- a/example/app/pages/demos/i18n/page.haml +++ b/example/app/pages/demos/i18n/page.haml @@ -1,16 +1,7 @@ :ruby Heading = import("/app/components/Layout/Heading") - T = translations("sv-SE", "en-US") - def lang - helpers.get_accepted_language(T.keys) - end - - def t(*path, **replacements) - value = T.fetch(lang) { raise "No translations!" }.dig(*path) - return "Missing translation for #{lang} at #{path.join(".")}!" unless value - format(value, replacements) - end + translations("sv-SE", "en-US") def handle_set_language(e) e => { target: { value: } } @@ -23,5 +14,5 @@ %p= t(:body) %p= t(:current_language, lang:) %div - = T.keys.map do |language| + = self.class.loaded_translations.keys.map do |language| %button(onclick=handle_set_language value=language)= language diff --git a/lib/mayu/component/base.rb b/lib/mayu/component/base.rb index 493a8a72..cf601bb6 100644 --- a/lib/mayu/component/base.rb +++ b/lib/mayu/component/base.rb @@ -43,6 +43,13 @@ def __mayu_resource? end end + LoadedTranslations = T.let({}.freeze, T::Hash[String, T.untyped]) + + sig { returns(T::Hash[String, T.untyped]) } + def self.loaded_translations + const_get(:LoadedTranslations) + end + sig do overridable .params(props: Component::Props, state: Component::State) @@ -52,6 +59,27 @@ def self.get_derived_state_from_props(props, state) {} end + sig { returns(String) } + def lang + helpers.get_accepted_language(self.class.loaded_translations.keys) or + raise "There are no translations!" + end + + sig { params(path: Symbol, replacements: String).returns(String) } + def t(*path, **replacements) + value = + self + .class + .loaded_translations + .fetch(lang) { return "No translations" } + .dig(*path) + + unless value + return "Missing translation for #{lang} at #{path.join(".")}!" + end + format(value, replacements) + end + sig { params(wrapper: Wrapper).void } def initialize(wrapper) @__wrapper = wrapper diff --git a/lib/mayu/resources/types/component.rb b/lib/mayu/resources/types/component.rb index e42b9175..e4eb56ec 100644 --- a/lib/mayu/resources/types/component.rb +++ b/lib/mayu/resources/types/component.rb @@ -37,18 +37,19 @@ def self.svg(path) impl end - sig do - params(locales: String).returns( - T::Hash[String, T::Hash[Symbol, T.untyped]] - ) - end + sig { params(locales: String).void } def self.translations(*locales) - locales.each_with_object({}) do |locale, hash| - __resource.import( - "./#{__resource.basename_without_extension}.intl.#{locale}.toml" - ) => Translations => impl - hash[locale] = impl.to_h - end + const_set( + :LoadedTranslations, + locales + .each_with_object({}) do |locale, hash| + __resource.import( + "./#{__resource.basename_without_extension}.intl.#{locale}.toml" + ) => Translations => impl + hash[locale] = impl.to_h.freeze + end + .freeze + ) end end end From 6092cdf6325558096f8bbbac1271f9f631444cba Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Fri, 11 Nov 2022 23:52:24 -0500 Subject: [PATCH 05/15] i18n: Add translations to a bunch of pages --- example/app/components/Form/Button.haml | 6 ++- example/app/components/Form/Fieldset.haml | 2 +- example/app/components/Layout/Badges.haml | 41 ++++++++++++++++ example/app/components/Layout/Footer.haml | 48 ++----------------- example/app/components/Layout/Languages.haml | 35 ++++++++++++++ .../Layout/Languages.intl.en-US.toml | 1 + .../Layout/Languages.intl.sv-SE.toml | 1 + example/app/pages/demos/life/page.haml | 41 ++++++++++++---- .../app/pages/demos/life/page.intl.en-US.toml | 7 +++ .../app/pages/demos/life/page.intl.sv-SE.toml | 7 +++ .../app/pages/demos/pokemon/Pagination.haml | 12 +++-- .../demos/pokemon/Pagination.intl.en-US.toml | 4 ++ .../demos/pokemon/Pagination.intl.sv-SE.toml | 4 ++ example/app/pages/demos/pokemon/page.haml | 6 ++- .../pages/demos/pokemon/page.intl.en-US.toml | 2 + .../pages/demos/pokemon/page.intl.sv-SE.toml | 2 + lib/mayu/component/base.rb | 2 +- 17 files changed, 158 insertions(+), 63 deletions(-) create mode 100644 example/app/components/Layout/Badges.haml create mode 100644 example/app/components/Layout/Languages.haml create mode 100644 example/app/components/Layout/Languages.intl.en-US.toml create mode 100644 example/app/components/Layout/Languages.intl.sv-SE.toml create mode 100644 example/app/pages/demos/life/page.intl.en-US.toml create mode 100644 example/app/pages/demos/life/page.intl.sv-SE.toml create mode 100644 example/app/pages/demos/pokemon/Pagination.intl.en-US.toml create mode 100644 example/app/pages/demos/pokemon/Pagination.intl.sv-SE.toml create mode 100644 example/app/pages/demos/pokemon/page.intl.en-US.toml create mode 100644 example/app/pages/demos/pokemon/page.intl.sv-SE.toml diff --git a/example/app/components/Form/Button.haml b/example/app/components/Form/Button.haml index 081c6fc4..72bc031b 100644 --- a/example/app/components/Form/Button.haml +++ b/example/app/components/Form/Button.haml @@ -29,5 +29,9 @@ cursor: not-allowed; } -%button.button{style: { __button_color: props.fetch(:color, "var(--accent-color)") }, **props.except(:color)} +%button.button{ + class: props[:class], + style: { __button_color: props.fetch(:color, "var(--accent-color)") }, + **props.except(:color) +} %slot diff --git a/example/app/components/Form/Fieldset.haml b/example/app/components/Form/Fieldset.haml index b1db7c95..66c83dd7 100644 --- a/example/app/components/Form/Fieldset.haml +++ b/example/app/components/Form/Fieldset.haml @@ -8,5 +8,5 @@ font-size: 0.9em; } -%fieldset.fieldset +%fieldset.fieldset{**props} %slot diff --git a/example/app/components/Layout/Badges.haml b/example/app/components/Layout/Badges.haml new file mode 100644 index 00000000..1fa1ad9c --- /dev/null +++ b/example/app/components/Layout/Badges.haml @@ -0,0 +1,41 @@ +:ruby + Badge = import("./Badge") + + BADGES = [ + { + href: "https://github.com/mayu-live/framework/search?l=Ruby&type=code", + alt: "top language", + src: "https://img.shields.io/github/languages/top/mayu-live/framework", + }, { + href: "https://github.com/mayu-live/framework/actions/workflows/ruby.yml", + alt: "Ruby build status", + src: "https://img.shields.io/github/workflow/status/mayu-live/framework/Ruby/main?label=ruby" + }, { + href: "https://github.com/mayu-live/framework/actions/workflows/node.js.yml", + alt: "JavaScript build status", + src: "https://img.shields.io/github/workflow/status/mayu-live/framework/Node.js%20CI/main?label=JavaScript", + }, { + href: "https://github.com/mayu-live/framework/blob/main/COPYING", + alt: "License: AGPL-3.0", + src: "https://img.shields.io/github/license/mayu-live/framework", + }, { + href: "https://status.mayu.live/", + alt: "uptime status", + src: "https://img.shields.io/badge/uptime-up-green", + }, { + href: "https://github.com/mayu-live/framework/stargazers", + alt: "GitHub Stars", + src: "https://img.shields.io/github/stars/mayu-live/framework?style=social", + } + ] + +:css + .badges { + display: flex; + flex-wrap: wrap; + flex-direction: row; + gap: 0.5em; + } +%p.badges + = BADGES.map do |badge| + %Badge{key: badge[:src], **badge} diff --git a/example/app/components/Layout/Footer.haml b/example/app/components/Layout/Footer.haml index 12d827fb..ade66b87 100644 --- a/example/app/components/Layout/Footer.haml +++ b/example/app/components/Layout/Footer.haml @@ -1,34 +1,7 @@ :ruby MaxWidth = import("/app/components/Layout/MaxWidth") - Badge = import("./Badge") - - BADGES = [ - { - href: "https://github.com/mayu-live/framework/search?l=Ruby&type=code", - alt: "top language", - src: "https://img.shields.io/github/languages/top/mayu-live/framework", - }, { - href: "https://github.com/mayu-live/framework/actions/workflows/ruby.yml", - alt: "Ruby build status", - src: "https://img.shields.io/github/workflow/status/mayu-live/framework/Ruby/main?label=ruby" - }, { - href: "https://github.com/mayu-live/framework/actions/workflows/node.js.yml", - alt: "JavaScript build status", - src: "https://img.shields.io/github/workflow/status/mayu-live/framework/Node.js%20CI/main?label=JavaScript", - }, { - href: "https://github.com/mayu-live/framework/blob/main/COPYING", - alt: "License: AGPL-3.0", - src: "https://img.shields.io/github/license/mayu-live/framework", - }, { - href: "https://status.mayu.live/", - alt: "uptime status", - src: "https://img.shields.io/badge/uptime-up-green", - }, { - href: "https://github.com/mayu-live/framework/stargazers", - alt: "GitHub Stars", - src: "https://img.shields.io/github/stars/mayu-live/framework?style=social", - } - ] + Languages = import("./Languages") + Badges = import("./Badges") :css .footer { @@ -53,18 +26,6 @@ color: #fff; opacity: 1; } - - .badges { - display: flex; - flex-wrap: wrap; - flex-direction: row; - gap: 0.5em; - } - - .links { - display: inline-flex; - } - %footer.footer{class: props[:class]} %MaxWidth.inner %p @@ -73,6 +34,5 @@ %a.link(href="https://github.com/mayu-live" target="_blank")< github.com/mayu-live - %p.badges - = BADGES.map do |badge| - %Badge{key: badge[:src], **badge} + %Languages + %Badges diff --git a/example/app/components/Layout/Languages.haml b/example/app/components/Layout/Languages.haml new file mode 100644 index 00000000..1276bd54 --- /dev/null +++ b/example/app/components/Layout/Languages.haml @@ -0,0 +1,35 @@ +:ruby + translations("en-US", "sv-SE") + + def handle_set_language(e) + e => { target: { value: } } + helpers.set_prefer_language(value) + end +:css + .languages { + display: flex; + gap: .5em; + } + + .title { + font-size: inherit; + margin: 0; + } + + .button { + border: 0; + background: transparent; + color: #fff; + cursor: pointer; + display: inline-block; + margin: 0; + padding: 0; + } + + .button:hover { + text-decoration: underline; + } +%section.languages + %h3.title= t(:title) + %button.button(onclick=handle_set_language value="sv-SE" lang="sv-SE") Svenska + %button.button(onclick=handle_set_language value="en-US" lang="en-US") English (US) diff --git a/example/app/components/Layout/Languages.intl.en-US.toml b/example/app/components/Layout/Languages.intl.en-US.toml new file mode 100644 index 00000000..32888440 --- /dev/null +++ b/example/app/components/Layout/Languages.intl.en-US.toml @@ -0,0 +1 @@ +title = "Change language:" diff --git a/example/app/components/Layout/Languages.intl.sv-SE.toml b/example/app/components/Layout/Languages.intl.sv-SE.toml new file mode 100644 index 00000000..be3e8368 --- /dev/null +++ b/example/app/components/Layout/Languages.intl.sv-SE.toml @@ -0,0 +1 @@ +title = "Byt språk:" diff --git a/example/app/pages/demos/life/page.haml b/example/app/pages/demos/life/page.haml index c4c11620..42379723 100644 --- a/example/app/pages/demos/life/page.haml +++ b/example/app/pages/demos/life/page.haml @@ -1,8 +1,11 @@ :ruby + translations("en-US", "sv-SE") + Heading = import("/app/components/Layout/Heading") Fieldset = import("/app/components/Form/Fieldset") Input = import("/app/components/Form/Input") Button = import("/app/components/Form/Button") + Icon = import("/app/components/UI/Icon") GameGrid = import("./GameGrid") MAX_SIZE = 20 @@ -137,19 +140,39 @@ .alive { background: #333; } .alive:hover { background: #666; } + .button { + display: flex; + align-items: center; + } + + .icon { + margin-right: .5em; + } + :ruby state => grid:, size:, running: -%article - %Heading(level=2) Game of life +%article(lang=lang) + %Heading(level=2)= t(:title) %Fieldset - %legend Controls + %legend= t(:controls) .buttons - %Button(onclick=handle_reset) Reset - %Button(onclick=handle_randomize) Randomize - %Button(onclick=handle_step disabled=running) Step - %Button(onclick=handle_toggle_running){ - color: running ? "var(--red)" : "var(--green)" - }= running ? "Stop" : "Start" + %Button.button(onclick=handle_reset) + %Icon.icon(name="refresh") + = t(:reset) + %Button.button(onclick=handle_randomize) + %Icon.icon(name="dice") + = t(:randomize) + %Button.button(onclick=handle_step disabled=running) + %Icon.icon(name="forward_step") + = t(:step) + = if running + %Button.button(onclick=handle_toggle_running color="var(--red)") + %Icon.icon(name="pause") + = t(:stop) + = unless running + %Button.button(onclick=handle_toggle_running color="var(--green)") + %Icon.icon(name="play") + = t(:start) %GameGrid(grid=grid size=size onmousedown=handle_toggle onmouseenter=handle_mouseenter) diff --git a/example/app/pages/demos/life/page.intl.en-US.toml b/example/app/pages/demos/life/page.intl.en-US.toml new file mode 100644 index 00000000..7afd3eed --- /dev/null +++ b/example/app/pages/demos/life/page.intl.en-US.toml @@ -0,0 +1,7 @@ +title = "Game of life" +controls = "Controls" +randomize = "Randomize" +reset = "Reset" +start = "Start" +step = "Step" +stop = "Stop" diff --git a/example/app/pages/demos/life/page.intl.sv-SE.toml b/example/app/pages/demos/life/page.intl.sv-SE.toml new file mode 100644 index 00000000..dc829f93 --- /dev/null +++ b/example/app/pages/demos/life/page.intl.sv-SE.toml @@ -0,0 +1,7 @@ +title = "Livets spel" +controls = "Kontroller" +randomize = "Slumpa" +reset = "Återställ" +start = "Starta" +step = "Stega" +stop = "Stanna" diff --git a/example/app/pages/demos/pokemon/Pagination.haml b/example/app/pages/demos/pokemon/Pagination.haml index fb475d43..15e05a2f 100644 --- a/example/app/pages/demos/pokemon/Pagination.haml +++ b/example/app/pages/demos/pokemon/Pagination.haml @@ -2,6 +2,8 @@ Fieldset = import("/app/components/Form/Fieldset") Button = import("/app/components/Form/Button") + translations("en-US", "sv-SE") + def pagination_window(current_page:, total_pages:, window_size: 5) half_window_size = (window_size - 1) / 2 first = current_page - half_window_size.ceil @@ -34,14 +36,14 @@ window_size: props[:window_size] || 5, ) -%Fieldset +%Fieldset(lang=lang) %legend - Page #{page} of #{total_pages.succ}, showing #{per_page} per page + = t(:legend, page:, total_pages: total_pages.succ, per_page:) .wrap %nav.buttons(aria-label="pagination") %a.button(href=prev_page_link rel="prev") - Previous page + = t(:previous_page) %ul.pages = pages.map do |num| @@ -52,10 +54,10 @@ }= num %a.button(href=next_page_link rel="prev") - Next page + = t(:next_page) .perPage - Per page: + = t(:per_page_label) %select(value=per_page){on_change: props[:on_change_per_page]}< = [20, 40, 80].map do |num| %option(key=num value=num)= num diff --git a/example/app/pages/demos/pokemon/Pagination.intl.en-US.toml b/example/app/pages/demos/pokemon/Pagination.intl.en-US.toml new file mode 100644 index 00000000..b363a63c --- /dev/null +++ b/example/app/pages/demos/pokemon/Pagination.intl.en-US.toml @@ -0,0 +1,4 @@ +legend = "Page %d of %d, showing %d per page" +previous_page = "Previous page" +next_page = "Next page" +per_page_label = "Per page:" diff --git a/example/app/pages/demos/pokemon/Pagination.intl.sv-SE.toml b/example/app/pages/demos/pokemon/Pagination.intl.sv-SE.toml new file mode 100644 index 00000000..87e64c64 --- /dev/null +++ b/example/app/pages/demos/pokemon/Pagination.intl.sv-SE.toml @@ -0,0 +1,4 @@ +legend = "Sida %d av %d, visar %d per sida" +previous_page = "Föregående sida" +next_page = "Nästa sida" +per_page_label = "Antal per sida:" diff --git a/example/app/pages/demos/pokemon/page.haml b/example/app/pages/demos/pokemon/page.haml index 9fc2422c..5f118ed1 100644 --- a/example/app/pages/demos/pokemon/page.haml +++ b/example/app/pages/demos/pokemon/page.haml @@ -1,6 +1,8 @@ :ruby Pagination = import("./Pagination") + translations("en-US", "sv-SE") + def self.get_initial_state(**props) = { result: nil, error: nil, @@ -37,11 +39,11 @@ state => result:, error: - return if error - %p Error: #{error} + %p= t(:error, error:) - return unless result %p - Loading Pokémon from + = t(:loading_pokemon_from) %a(href="https://pokeapi.co/")< PokéAPI :ruby diff --git a/example/app/pages/demos/pokemon/page.intl.en-US.toml b/example/app/pages/demos/pokemon/page.intl.en-US.toml new file mode 100644 index 00000000..a97ef61e --- /dev/null +++ b/example/app/pages/demos/pokemon/page.intl.en-US.toml @@ -0,0 +1,2 @@ +loading_pokemon_from = "Loading Pokémon from" +error = "Error: %s" diff --git a/example/app/pages/demos/pokemon/page.intl.sv-SE.toml b/example/app/pages/demos/pokemon/page.intl.sv-SE.toml new file mode 100644 index 00000000..4e4acaac --- /dev/null +++ b/example/app/pages/demos/pokemon/page.intl.sv-SE.toml @@ -0,0 +1,2 @@ +loading_pokemon_from = "Laddar Pokémon från" +error = "Error: %s" diff --git a/lib/mayu/component/base.rb b/lib/mayu/component/base.rb index cf601bb6..cb5033d6 100644 --- a/lib/mayu/component/base.rb +++ b/lib/mayu/component/base.rb @@ -65,7 +65,7 @@ def lang raise "There are no translations!" end - sig { params(path: Symbol, replacements: String).returns(String) } + sig { params(path: Symbol, replacements: T.untyped).returns(String) } def t(*path, **replacements) value = self From 7582193a16f44ff7cffd9dda6a129be8e5351584 Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Sat, 12 Nov 2022 00:21:27 -0500 Subject: [PATCH 06/15] Rerender pages when language changes This does not rerender all components though. I'm going to try to pass translations into the component instance kinda like props... --- lib/mayu/environment.rb | 18 +++++++++++++----- lib/mayu/session.rb | 22 ++++++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/mayu/environment.rb b/lib/mayu/environment.rb index 60fb87c3..a5852400 100644 --- a/lib/mayu/environment.rb +++ b/lib/mayu/environment.rb @@ -101,11 +101,13 @@ def create_store(initial_state: {}) end sig do - params(request_path: String, headers: T::Hash[String, String]).returns( - VDOM::Descriptor - ) + params( + request_path: String, + accept_language: AcceptLanguage::Parser, + headers: T::Hash[String, String] + ).returns(VDOM::Descriptor) end - def load_root(request_path, headers: {}) + def load_root(request_path, accept_language:, headers: {}) path, search = request_path.split("?", 2) # We should match the route earlier, so that we don't have to get this # far in case it doesn't match... @@ -123,7 +125,13 @@ def load_root(request_path, headers: {}) resources.load_resource(File.join("/", "app", "root")).type => Resources::Types::Component => root - request_info = { path:, params:, query:, headers: }.freeze + request_info = { + path:, + params:, + query:, + headers:, + accept_language: + }.freeze # Apply the layouts. # NOTE: Pages should probably be their own diff --git a/lib/mayu/session.rb b/lib/mayu/session.rb index f0239c1a..9f50d860 100644 --- a/lib/mayu/session.rb +++ b/lib/mayu/session.rb @@ -139,10 +139,16 @@ def initialize( store: nil ) @environment = environment + @id = T.let(Nanoid.generate, String) @token = T.let(self.class.generate_token, String) + @path = path @headers = headers + + @prefer_language = prefer_language + @accept_language = T.let(nil, T.nilable(AcceptLanguage::Parser)) + @vtree = T.let(vtree || VDOM::VTree.new(session: self), VDOM::VTree) @log = T.let(EventStream::Log.new, EventStream::Log) @store = @@ -150,11 +156,14 @@ def initialize( store || environment.create_store(initial_state: {}), State::Store ) - @app = T.let(environment.load_root(path, headers:), VDOM::Descriptor) + + @app = + T.let( + environment.load_root(path, headers:, accept_language:), + VDOM::Descriptor + ) @last_ping_at = T.let(Time.now.to_f, Float) @barrier = T.let(Async::Barrier.new, Async::Barrier) - @prefer_language = prefer_language - @accept_language = T.let(nil, T.nilable(AcceptLanguage::Parser)) end sig { returns(AcceptLanguage::Parser) } @@ -280,7 +289,7 @@ def marshal_load(a) @last_ping_at = Time.now.to_f @vtree = VDOM::Marshalling.restore(dumped_vtree, session: self) @store = @environment.create_store(initial_state: Marshal.restore(state)) - @app = @environment.load_root(@path, headers:) + @app = @environment.load_root(@path, headers:, accept_language:) @barrier = Async::Barrier.new @log = EventStream::Log.new end @@ -312,14 +321,14 @@ def handle_callback(callback_id, payload = {}) sig { void } def rerender - @app = @environment.load_root(path, headers:) + @app = @environment.load_root(path, headers:, accept_language:) @vtree.replace_root(@app) end sig { params(path: String).void } def navigate(path) Console.logger.info(self, "navigate: #{path.inspect}") - @app = @environment.load_root(path, headers:) + @app = @environment.load_root(path, headers:, accept_language:) @path = path @vtree.replace_root(@app) end @@ -370,6 +379,7 @@ def run(task: Async::Task.current, &block) yield [:action, payload] in [:set_prefer_language, language] self.prefer_language = language + rerender yield [:set_prefer_language, language] in [:update_finished, *] # noop From af05dce9e7f4c09b8f7d0e794a672daf6416c957 Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Sat, 12 Nov 2022 01:21:43 -0500 Subject: [PATCH 07/15] i18n: More translations --- example/app/pages/demos/layout.haml | 34 ++++++++++--------- .../app/pages/demos/layout.intl.en-US.toml | 12 +++++++ .../app/pages/demos/layout.intl.sv-SE.toml | 12 +++++++ example/app/pages/demos/svg/page.haml | 8 ++--- .../app/pages/demos/svg/page.intl.en-US.toml | 5 +++ .../app/pages/demos/svg/page.intl.sv-SE.toml | 5 +++ .../app/pages/demos/tree/FileContents.haml | 10 +++--- .../demos/tree/FileContents.intl.en-US.toml | 4 +++ .../demos/tree/FileContents.intl.sv-SE.toml | 4 +++ example/app/pages/demos/tree/page.haml | 4 ++- .../app/pages/demos/tree/page.intl.en-US.toml | 1 + .../app/pages/demos/tree/page.intl.en.toml | 1 + .../app/pages/demos/tree/page.intl.sv-SE.toml | 1 + .../app/pages/demos/tree/page.intl.sv.toml | 1 + 14 files changed, 77 insertions(+), 25 deletions(-) create mode 100644 example/app/pages/demos/layout.intl.en-US.toml create mode 100644 example/app/pages/demos/layout.intl.sv-SE.toml create mode 100644 example/app/pages/demos/svg/page.intl.en-US.toml create mode 100644 example/app/pages/demos/svg/page.intl.sv-SE.toml create mode 100644 example/app/pages/demos/tree/FileContents.intl.en-US.toml create mode 100644 example/app/pages/demos/tree/FileContents.intl.sv-SE.toml create mode 100644 example/app/pages/demos/tree/page.intl.en-US.toml create mode 100644 example/app/pages/demos/tree/page.intl.en.toml create mode 100644 example/app/pages/demos/tree/page.intl.sv-SE.toml create mode 100644 example/app/pages/demos/tree/page.intl.sv.toml diff --git a/example/app/pages/demos/layout.haml b/example/app/pages/demos/layout.haml index bf7ebd0a..35bbf3f6 100644 --- a/example/app/pages/demos/layout.haml +++ b/example/app/pages/demos/layout.haml @@ -4,19 +4,21 @@ MenuItem = import("/app/components/Layout/MenuItem") Breadcrumbs = import("/app/components/UI/Breadcrumbs") + translations("sv-SE", "en-US") + LINKS = { - "/demos" => "Demos", - "/demos/i18n" => "I18n", - "/demos/pokemon" => "Pokémon", - "/demos/tree" => "App tree", - "/demos/form" => "Form elements", - "/demos/images" => "Images", - "/demos/svg" => "SVGs", - "/demos/fuzzy-matcher" => "Fuzzy matcher", - "/demos/todo" => "Todo app", - "/demos/exceptions" => "Exceptions", - "/demos/life" => "Game of life", - "/demos/events" => "Events", + "/demos" => :demos, + "/demos/i18n" => :i18n, + "/demos/pokemon" => :pokemon, + "/demos/tree" => :tree, + "/demos/form" => :form, + "/demos/images" => :images, + "/demos/svg" => :svg, + "/demos/fuzzy-matcher" => :fuzzymatch, + "/demos/todo" => :todo, + "/demos/exceptions" => :exceptions, + "/demos/life" => :life, + "/demos/events" => :events, } def breadcrumb_links @@ -27,7 +29,7 @@ LINKS.select { s = split_path(_1) s == splat.slice(0, s.length) - } + }.transform_values { t(_1) } end def split_path(path) @@ -45,6 +47,6 @@ %Breadcrumbs(slot="breadcrumbs" links=breadcrumb_links) .nav(slot="menu") - %Menu - = LINKS.map do |path, text| - %MenuItem(key=path href=path)= text + %Menu(lang=lang) + = LINKS.map do |path, label| + %MenuItem(key=path href=path)= t(label) diff --git a/example/app/pages/demos/layout.intl.en-US.toml b/example/app/pages/demos/layout.intl.en-US.toml new file mode 100644 index 00000000..18c95729 --- /dev/null +++ b/example/app/pages/demos/layout.intl.en-US.toml @@ -0,0 +1,12 @@ +demos = "Demos" +i18n = "Translations" +pokemon = "Pokémon" +tree = "App tree" +form = "Form elements" +images = "Images" +svg = "Scalable Vector Graphics" +fuzzymatch = "Fuzzy matcher" +todo = "Todo app" +life = "Game of life" +events = "Events" +exceptions = "Exceptions" diff --git a/example/app/pages/demos/layout.intl.sv-SE.toml b/example/app/pages/demos/layout.intl.sv-SE.toml new file mode 100644 index 00000000..6b0c9a5e --- /dev/null +++ b/example/app/pages/demos/layout.intl.sv-SE.toml @@ -0,0 +1,12 @@ +demos = "Demos" +i18n = "Översättningar" +pokemon = "Pokémon" +tree = "Applikationsträd" +form = "Formulärelement" +images = "Bilder" +svg = "Scalable Vector Graphics" +fuzzymatch = "Fuzzymatchning" +todo = "Att göra-app" +life = "Livets spel (Game of life)" +events = "Händelser" +exceptions = "Exceptions" diff --git a/example/app/pages/demos/svg/page.haml b/example/app/pages/demos/svg/page.haml index 0d554b03..c85c588e 100644 --- a/example/app/pages/demos/svg/page.haml +++ b/example/app/pages/demos/svg/page.haml @@ -2,6 +2,8 @@ Heading = import("/app/components/Layout/Heading") Clock = import("/app/components/Clock") + translations("sv-SE", "en-US") + PATHS = [ "M 10 10 C 20 20, 40 20, 50 10", "M 70 10 C 70 20, 110 20, 110 10", @@ -35,10 +37,8 @@ } %article - %Heading(level=2) SVG demo - %p - This example shows some SVGs. - The time is updated on the server. + %Heading(level=2)= t(:title) + %p= t(:description) .flex .col %Clock diff --git a/example/app/pages/demos/svg/page.intl.en-US.toml b/example/app/pages/demos/svg/page.intl.en-US.toml new file mode 100644 index 00000000..b3b52d70 --- /dev/null +++ b/example/app/pages/demos/svg/page.intl.en-US.toml @@ -0,0 +1,5 @@ +title = "SVG demo" +description = """ +This example shows some SVGs. +The clock is updated on the server. +""" diff --git a/example/app/pages/demos/svg/page.intl.sv-SE.toml b/example/app/pages/demos/svg/page.intl.sv-SE.toml new file mode 100644 index 00000000..420b0913 --- /dev/null +++ b/example/app/pages/demos/svg/page.intl.sv-SE.toml @@ -0,0 +1,5 @@ +title = "SVG demo" +description = """ +Detta exemplet visar några SVGs. +Klockan uppdateras på servern. +""" diff --git a/example/app/pages/demos/tree/FileContents.haml b/example/app/pages/demos/tree/FileContents.haml index 813026c1..99baa822 100644 --- a/example/app/pages/demos/tree/FileContents.haml +++ b/example/app/pages/demos/tree/FileContents.haml @@ -1,6 +1,8 @@ :ruby Highlight = import("/app/components/Layout/Highlight") + translations("en-US", "sv-SE") + ALLOWED_EXTENSIONS = %w[.rb .css .haml] :css @@ -26,22 +28,22 @@ - props => { root:, path: } - return unless path - %p Please choose a file + %p= t(:please_choose_a_file) - path = File.expand_path(path, "/") - return unless path.start_with?("/app/") - %p #{path} is not valid + %p= t(:invalid_path, path:) - absolute_path = File.join(root, path) - basename = File.basename(path) - return unless File.file?(absolute_path) - %p #{basename} is not a file + %p= t(:path_is_not_a_file, path:) - extname = File.extname(basename) - return unless ALLOWED_EXTENSIONS.include?(extname) - %p #{path} is not a #{ALLOWED_EXTENSIONS.join("/")}-file + %p= t(:unallowed_extension, path:, extensions: ALLOWED_EXTENSIONS.join("/")) :ruby source = File.read(absolute_path) diff --git a/example/app/pages/demos/tree/FileContents.intl.en-US.toml b/example/app/pages/demos/tree/FileContents.intl.en-US.toml new file mode 100644 index 00000000..c5e5c73c --- /dev/null +++ b/example/app/pages/demos/tree/FileContents.intl.en-US.toml @@ -0,0 +1,4 @@ +please_choose_a_file = "Please choose a file." +invalid_path = "%s is not valid." +not_a_file = "%s is not a file." +unallowed_extension = "%s is not a %s-file." diff --git a/example/app/pages/demos/tree/FileContents.intl.sv-SE.toml b/example/app/pages/demos/tree/FileContents.intl.sv-SE.toml new file mode 100644 index 00000000..6e8054c3 --- /dev/null +++ b/example/app/pages/demos/tree/FileContents.intl.sv-SE.toml @@ -0,0 +1,4 @@ +please_choose_a_file = "Vänligen välj en fil." +invalid_path = "%s är inte tillåten." +path_is_not_a_file = "%s är inte en fil." +unallowed_extension = "%s är inte en %s-fil." diff --git a/example/app/pages/demos/tree/page.haml b/example/app/pages/demos/tree/page.haml index 31ba9780..56cdb12a 100644 --- a/example/app/pages/demos/tree/page.haml +++ b/example/app/pages/demos/tree/page.haml @@ -1,6 +1,8 @@ :ruby Heading = import("/app/components/Layout/Heading") + translations("en-US", "sv-SE") + Entry = import("./Entry") FileContents = import("./FileContents") @@ -40,7 +42,7 @@ %Heading(level=2) App tree %p - This page shows the file structure of the example app. + = t(:description) %a(href="https://github.com/mayu-live/framework/tree/main/example/app")< .browser diff --git a/example/app/pages/demos/tree/page.intl.en-US.toml b/example/app/pages/demos/tree/page.intl.en-US.toml new file mode 100644 index 00000000..169d39c3 --- /dev/null +++ b/example/app/pages/demos/tree/page.intl.en-US.toml @@ -0,0 +1 @@ +description = "This page shows the file structure of the example app." diff --git a/example/app/pages/demos/tree/page.intl.en.toml b/example/app/pages/demos/tree/page.intl.en.toml new file mode 100644 index 00000000..169d39c3 --- /dev/null +++ b/example/app/pages/demos/tree/page.intl.en.toml @@ -0,0 +1 @@ +description = "This page shows the file structure of the example app." diff --git a/example/app/pages/demos/tree/page.intl.sv-SE.toml b/example/app/pages/demos/tree/page.intl.sv-SE.toml new file mode 100644 index 00000000..374e6af6 --- /dev/null +++ b/example/app/pages/demos/tree/page.intl.sv-SE.toml @@ -0,0 +1 @@ +description = "Denna sidan visar filstrukturen i exempelapplikationen." diff --git a/example/app/pages/demos/tree/page.intl.sv.toml b/example/app/pages/demos/tree/page.intl.sv.toml new file mode 100644 index 00000000..374e6af6 --- /dev/null +++ b/example/app/pages/demos/tree/page.intl.sv.toml @@ -0,0 +1 @@ +description = "Denna sidan visar filstrukturen i exempelapplikationen." From ed184e11f75db9748b5f6bc35bab287156825151 Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Sat, 12 Nov 2022 01:44:40 -0500 Subject: [PATCH 08/15] I18n: Add more translations --- example/app/pages/demos/events/page.haml | 15 +++++----- .../pages/demos/events/page.intl.en-US.toml | 7 +++++ .../pages/demos/events/page.intl.sv-SE.toml | 7 +++++ example/app/pages/demos/page.haml | 6 ++-- example/app/pages/demos/page.intl.en-US.toml | 2 ++ example/app/pages/demos/todo/page.haml | 28 ++++++++++--------- .../app/pages/demos/todo/page.intl.en-US.toml | 11 ++++++++ .../app/pages/demos/todo/page.intl.sv-SE.toml | 11 ++++++++ 8 files changed, 64 insertions(+), 23 deletions(-) create mode 100644 example/app/pages/demos/events/page.intl.en-US.toml create mode 100644 example/app/pages/demos/events/page.intl.sv-SE.toml create mode 100644 example/app/pages/demos/page.intl.en-US.toml create mode 100644 example/app/pages/demos/todo/page.intl.en-US.toml create mode 100644 example/app/pages/demos/todo/page.intl.sv-SE.toml diff --git a/example/app/pages/demos/events/page.haml b/example/app/pages/demos/events/page.haml index 372e9911..3a372aa3 100644 --- a/example/app/pages/demos/events/page.haml +++ b/example/app/pages/demos/events/page.haml @@ -1,4 +1,6 @@ :ruby + translations("sv-SE", "en-US") + Heading = import("/app/components/Layout/Heading") Highlight = import("/app/components/Layout/Highlight") Card = import("/app/components/Layout/Card") @@ -9,17 +11,14 @@ } def handle_alert(e) - helpers.alert("Hello world\nCount: #{state[:count]}") + helpers.alert(t(:message, count: state[:count])) update do |state| { count: state[:count] + 1 } end end -%article - %Heading(level=2) Events - %p This page contains some experiments with events - - %button(onclick=handle_alert) Alert - - +%article(lang=lang) + %Heading(level=2)= t(:title) + %p= t(:description) + %button(onclick=handle_alert)= t(:alert) diff --git a/example/app/pages/demos/events/page.intl.en-US.toml b/example/app/pages/demos/events/page.intl.en-US.toml new file mode 100644 index 00000000..7cf56902 --- /dev/null +++ b/example/app/pages/demos/events/page.intl.en-US.toml @@ -0,0 +1,7 @@ +title = "Events" +description = "This page contains some experiments with events." +alert = "Alert" +message = """ +Hello world! +Counter: %d +""" diff --git a/example/app/pages/demos/events/page.intl.sv-SE.toml b/example/app/pages/demos/events/page.intl.sv-SE.toml new file mode 100644 index 00000000..b3f75fe3 --- /dev/null +++ b/example/app/pages/demos/events/page.intl.sv-SE.toml @@ -0,0 +1,7 @@ +title = "Händelser" +description = "Denna sida innehåller experiment med events." +alert = "Alert" +message = """ +Hej världen! +Räknare: %d +""" diff --git a/example/app/pages/demos/page.haml b/example/app/pages/demos/page.haml index e410dd64..0823f51d 100644 --- a/example/app/pages/demos/page.haml +++ b/example/app/pages/demos/page.haml @@ -1,9 +1,11 @@ :ruby + translations("sv-SE", "en-US") + Heading = import("/app/components/Layout/Heading") ButtonGame = import("./ButtonGame") %article - %Heading(level=2) Demos index - %p This section demonstrates some of the capabilities of Mayu. + %Heading(level=2)= t(:title) + %p= t(:description) %ButtonGame diff --git a/example/app/pages/demos/page.intl.en-US.toml b/example/app/pages/demos/page.intl.en-US.toml new file mode 100644 index 00000000..0233ab82 --- /dev/null +++ b/example/app/pages/demos/page.intl.en-US.toml @@ -0,0 +1,2 @@ +title = "Demos index" +description = "This section demonstrates some of the capabilities of Mayu." diff --git a/example/app/pages/demos/todo/page.haml b/example/app/pages/demos/todo/page.haml index d41c9ffa..1b6bed79 100644 --- a/example/app/pages/demos/todo/page.haml +++ b/example/app/pages/demos/todo/page.haml @@ -1,4 +1,6 @@ :ruby + translations("en-US", "sv-SE") + Card = import("/app/components/Layout/Card") Link = import("/app/components/UI/Link") @@ -115,15 +117,11 @@ end def pluralize(count, singular, plural) - format( - "%d %s", - count, - if count == 1 - singular - else - plural - end - ) + if count == 1 + singular + else + plural + end end def toggle_checkbox_state @@ -297,7 +295,9 @@ \. %form.form(onsubmit=handle_submit){key: state[:form_key]} %input.toggle-all(type="checkbox" onchange=handle_toggle_all){**toggle_checkbox_state} - %input.input(required autofocus name="new_todo" placeholder="What needs to be done?" value="") + %input.input(required autofocus name="new_todo" value=""){ + placeholder: t(:what_needs_to_be_done) + } .main %ul.list = get_filtered_items.map do |item| @@ -320,13 +320,15 @@ %button.delete-button(onclick=handle_delete){name: item[:id]} .footer .footer-grid - .items-left #{pluralize(get_filtered_items("active").size, "item", "items")} left + .items-left + - count = get_filtered_items("active").size + = t(:item, pluralize(count, :one, :many), count:) .filter = %w[all active completed].map do |name| %button.filter-button(onclick=handle_set_filter name=name title="Show #{name} items"){ aria: { current: (state[:filter] == name).to_s } - }= name.capitalize + }= t(:filter, name.to_sym) .clear-completed - completed = get_filtered_items("completed") = if completed.length.nonzero? - %button.clear-button(onclick=handle_clear_completed) Clear completed + %button.clear-button(onclick=handle_clear_completed)= t(:clear_completed) diff --git a/example/app/pages/demos/todo/page.intl.en-US.toml b/example/app/pages/demos/todo/page.intl.en-US.toml new file mode 100644 index 00000000..3324d9bb --- /dev/null +++ b/example/app/pages/demos/todo/page.intl.en-US.toml @@ -0,0 +1,11 @@ +what_needs_to_be_done = "What needs to be done?" +clear_completed = "Clear completed" + +[item] + one = "%d item" + many = "%d items" + +[filter] + all = "All" + active = "Active" + completed = "Completed" diff --git a/example/app/pages/demos/todo/page.intl.sv-SE.toml b/example/app/pages/demos/todo/page.intl.sv-SE.toml new file mode 100644 index 00000000..f4b8d2b7 --- /dev/null +++ b/example/app/pages/demos/todo/page.intl.sv-SE.toml @@ -0,0 +1,11 @@ +what_needs_to_be_done = "Vad behöver göras?" +clear_completed = "Rensa färdiga" + +[item] + one = "%d sak" + many = "%d saker" + +[filter] + all = "Alla" + active = "Aktiva" + completed = "Färdiga" From e84541d693c48c43064ec4e3255be64024db1c61 Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Sat, 12 Nov 2022 06:20:20 -0500 Subject: [PATCH 09/15] i18n: more translations --- Gemfile.lock | 10 +++++ example/Gemfile.lock | 10 +++++ example/app/components/Layout/Header.haml | 12 +++--- .../components/Layout/Header.intl.en-US.toml | 4 ++ .../components/Layout/Header.intl.sv-SE.toml | 4 ++ example/app/components/Layout/Languages.haml | 2 +- example/app/components/Layout/Menu.haml | 2 +- example/app/pages/demos/i18n/page.haml | 41 +++++++++++++++++-- .../app/pages/demos/i18n/page.intl.en-US.toml | 3 ++ .../app/pages/demos/i18n/page.intl.sv-SE.toml | 3 ++ example/app/pages/demos/layout.haml | 2 +- example/app/pages/demos/page.haml | 2 + example/app/pages/demos/page.intl.sv-SE.toml | 2 + .../app/pages/demos/todo/page.intl.en-US.toml | 4 +- .../app/pages/demos/todo/page.intl.sv-SE.toml | 4 +- example/app/pages/demos/tree/page.haml | 4 +- .../app/pages/demos/tree/page.intl.en-US.toml | 1 + .../app/pages/demos/tree/page.intl.sv-SE.toml | 1 + lib/mayu/component/base.rb | 7 +--- lib/mayu/component/wrapper.rb | 5 +++ lib/mayu/vdom/vnode.rb | 19 ++++++++- lib/mayu/vdom/vtree.rb | 3 +- mayu-live.gemspec | 1 + 23 files changed, 121 insertions(+), 25 deletions(-) create mode 100644 example/app/components/Layout/Header.intl.en-US.toml create mode 100644 example/app/components/Layout/Header.intl.sv-SE.toml create mode 100644 example/app/pages/demos/page.intl.sv-SE.toml diff --git a/Gemfile.lock b/Gemfile.lock index 22209347..c7a34daa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -29,6 +29,7 @@ PATH syntax_tree-xml (~> 0.1.0) terminal-table (~> 3.0.1) toml-rb (~> 2.2.0) + twitter_cldr (~> 6.11.4) GEM remote: https://rubygems.org/ @@ -57,8 +58,11 @@ GEM async (>= 1.25) brotli (0.4.0) builder (3.2.4) + camertron-eprun (1.1.1) citrus (3.0.2) + cldr-plurals-runtime-rb (1.1.0) coderay (1.1.3) + concurrent-ruby (1.1.10) console (1.15.3) fiber-local crass (1.0.6) @@ -195,6 +199,12 @@ GEM toml-rb (2.2.0) citrus (~> 3.0, > 3.0) traces (0.7.0) + twitter_cldr (6.11.4) + camertron-eprun + cldr-plurals-runtime-rb (~> 1.1) + tzinfo + tzinfo (2.0.5) + concurrent-ruby (~> 1.0) unicode-display_width (2.3.0) unparser (0.6.5) diff-lcs (~> 1.3) diff --git a/example/Gemfile.lock b/example/Gemfile.lock index 48cf341e..c69eb8b4 100644 --- a/example/Gemfile.lock +++ b/example/Gemfile.lock @@ -29,6 +29,7 @@ PATH syntax_tree-xml (~> 0.1.0) terminal-table (~> 3.0.1) toml-rb (~> 2.2.0) + twitter_cldr (~> 6.11.4) GEM remote: https://rubygems.org/ @@ -54,8 +55,11 @@ GEM async-pool (0.3.12) async (>= 1.25) brotli (0.4.0) + camertron-eprun (1.1.1) citrus (3.0.2) + cldr-plurals-runtime-rb (1.1.0) coderay (1.1.3) + concurrent-ruby (1.1.10) console (1.15.3) fiber-local crass (1.0.6) @@ -126,6 +130,12 @@ GEM toml-rb (2.2.0) citrus (~> 3.0, > 3.0) traces (0.7.0) + twitter_cldr (6.11.4) + camertron-eprun + cldr-plurals-runtime-rb (~> 1.1) + tzinfo + tzinfo (2.0.5) + concurrent-ruby (~> 1.0) unicode-display_width (2.3.0) PLATFORMS diff --git a/example/app/components/Layout/Header.haml b/example/app/components/Layout/Header.haml index 161fa3cb..f346c6fd 100644 --- a/example/app/components/Layout/Header.haml +++ b/example/app/components/Layout/Header.haml @@ -1,20 +1,22 @@ :ruby + translations("sv-SE", "en-US") + MaxWidth = import("/app/components/Layout/MaxWidth") Icon = import("/app/components/UI/Icon") -%header{class: props[:class]} +%header(lang=lang){class: props[:class]} %MaxWidth.inner - %a.titleLink(href="/" title="Start page") + %a.titleLink(href="/"){title: t(:startpage)} Mayu Live %nav.nav %menu.menu %li %a.navLink(href="/demos") - Demos + = t(:demos) %li %a.navLink(href="/docs") - Docs + = t(:docs) %li %a.navLink(href="https://github.com/mayu-live/framework" target="_blank") GitHub - %Icon.icon(name="up-right-from-square") + %Icon.icon(icon="up-right-from-square") diff --git a/example/app/components/Layout/Header.intl.en-US.toml b/example/app/components/Layout/Header.intl.en-US.toml new file mode 100644 index 00000000..2c8ffd9d --- /dev/null +++ b/example/app/components/Layout/Header.intl.en-US.toml @@ -0,0 +1,4 @@ +startpage = "Startsida" +demos = "Demos" +docs = "Docs" +github = "GitHub" diff --git a/example/app/components/Layout/Header.intl.sv-SE.toml b/example/app/components/Layout/Header.intl.sv-SE.toml new file mode 100644 index 00000000..38bb9702 --- /dev/null +++ b/example/app/components/Layout/Header.intl.sv-SE.toml @@ -0,0 +1,4 @@ +startpage = "Startsida" +demos = "Demo" +docs = "Dokumentation" +github = "GitHub" diff --git a/example/app/components/Layout/Languages.haml b/example/app/components/Layout/Languages.haml index 1276bd54..9297c4f0 100644 --- a/example/app/components/Layout/Languages.haml +++ b/example/app/components/Layout/Languages.haml @@ -29,7 +29,7 @@ .button:hover { text-decoration: underline; } -%section.languages +%section.languages(lang=lang) %h3.title= t(:title) %button.button(onclick=handle_set_language value="sv-SE" lang="sv-SE") Svenska %button.button(onclick=handle_set_language value="en-US" lang="en-US") English (US) diff --git a/example/app/components/Layout/Menu.haml b/example/app/components/Layout/Menu.haml index 1b5eba13..06dd0a48 100644 --- a/example/app/components/Layout/Menu.haml +++ b/example/app/components/Layout/Menu.haml @@ -8,5 +8,5 @@ flex-direction: var(--menu-direction, column); gap: 0 1em; } -%menu.menu +%menu.menu{lang: props[:htmllang]} %slot diff --git a/example/app/pages/demos/i18n/page.haml b/example/app/pages/demos/i18n/page.haml index 77d4a088..630de00f 100644 --- a/example/app/pages/demos/i18n/page.haml +++ b/example/app/pages/demos/i18n/page.haml @@ -1,5 +1,8 @@ :ruby + require "twitter_cldr" + Heading = import("/app/components/Layout/Heading") + Fieldset = import("/app/components/Form/Fieldset") translations("sv-SE", "en-US") @@ -7,12 +10,44 @@ e => { target: { value: } } helpers.set_prefer_language(value) end +:css + .buttons { + display: flex; + gap: .5em; + } + .button[aria-current="true"] { + font-weight: bold; + } %article(lang=lang) %Heading(level=1)= t(:title) %p= t(:body) %p= t(:current_language, lang:) - %div - = self.class.loaded_translations.keys.map do |language| - %button(onclick=handle_set_language value=language)= language + + %ul + %dt Number + %dd= TwitterCldr::Localized::LocalizedNumber.new(1337, lang).to_s + %dt Currency + %dd= TwitterCldr::Localized::LocalizedNumber.new(1337, lang).to_currency.to_s(currency: "EUR") + %dt Currency + %dd= TwitterCldr::Localized::LocalizedNumber.new(12341337, lang).to_currency.to_s(currency: "COP") + %dt Short format + %dd= TwitterCldr::Localized::LocalizedNumber.new(2337, lang).to_decimal.to_s(format: :short) + %dt Long format + %dd= TwitterCldr::Localized::LocalizedNumber.new(2337, lang).to_decimal.to_s(format: :long) + %dt Short time + %dd= TwitterCldr::Localized::LocalizedDateTime.new(DateTime.now, lang).to_short_s + %dt Formatted time + %dd= TwitterCldr::Localized::LocalizedDateTime.new(DateTime.now, lang).to_additional_s("EBhms") + %dt List + %dd= TwitterCldr::Formatters::ListFormatter.new(lang).format(["Larry", "Curly", "Moe"]) + %dt Plural rules + %dd= TwitterCldr::Formatters::Plurals::Rules.all_for(lang).inspect + %Fieldset + %legend= t(:change_language) + %div.buttons + = [*self.class.loaded_translations.keys, "sv", "en"].map do |language| + %button.button(onclick=handle_set_language value=language title=language){ + aria: { current: (props[:lang] == language).to_s }, + }= TwitterCldr::Shared::Languages.from_code_for_locale(language.to_sym, lang).capitalize diff --git a/example/app/pages/demos/i18n/page.intl.en-US.toml b/example/app/pages/demos/i18n/page.intl.en-US.toml index 796b790b..de60ffe5 100644 --- a/example/app/pages/demos/i18n/page.intl.en-US.toml +++ b/example/app/pages/demos/i18n/page.intl.en-US.toml @@ -3,3 +3,6 @@ body = """ hello world """ current_language = "Current language: %s" +change_language = "Change language" +lang.sv-SE = "Swedish" +lang.en-US = "English" diff --git a/example/app/pages/demos/i18n/page.intl.sv-SE.toml b/example/app/pages/demos/i18n/page.intl.sv-SE.toml index 3665dad6..6b4c2f5f 100644 --- a/example/app/pages/demos/i18n/page.intl.sv-SE.toml +++ b/example/app/pages/demos/i18n/page.intl.sv-SE.toml @@ -3,3 +3,6 @@ body = """ hej världen """ current_language = "Nuvarande språk: %s" +change_language = "Byt språk" +lang.sv-SE = "Svenska" +lang.en-US = "Engelska" diff --git a/example/app/pages/demos/layout.haml b/example/app/pages/demos/layout.haml index 35bbf3f6..d1c0bcfb 100644 --- a/example/app/pages/demos/layout.haml +++ b/example/app/pages/demos/layout.haml @@ -47,6 +47,6 @@ %Breadcrumbs(slot="breadcrumbs" links=breadcrumb_links) .nav(slot="menu") - %Menu(lang=lang) + %Menu(htmllang=lang) = LINKS.map do |path, label| %MenuItem(key=path href=path)= t(label) diff --git a/example/app/pages/demos/page.haml b/example/app/pages/demos/page.haml index 0823f51d..f8a8137a 100644 --- a/example/app/pages/demos/page.haml +++ b/example/app/pages/demos/page.haml @@ -8,4 +8,6 @@ %Heading(level=2)= t(:title) %p= t(:description) + %pre props[:lang] = #{props[:lang].inspect} + %ButtonGame diff --git a/example/app/pages/demos/page.intl.sv-SE.toml b/example/app/pages/demos/page.intl.sv-SE.toml new file mode 100644 index 00000000..56249c18 --- /dev/null +++ b/example/app/pages/demos/page.intl.sv-SE.toml @@ -0,0 +1,2 @@ +title = "Demos index" +description = "Denna sektion visar vad man kan göra med Mayu." diff --git a/example/app/pages/demos/todo/page.intl.en-US.toml b/example/app/pages/demos/todo/page.intl.en-US.toml index 3324d9bb..2d2e28eb 100644 --- a/example/app/pages/demos/todo/page.intl.en-US.toml +++ b/example/app/pages/demos/todo/page.intl.en-US.toml @@ -2,8 +2,8 @@ what_needs_to_be_done = "What needs to be done?" clear_completed = "Clear completed" [item] - one = "%d item" - many = "%d items" + one = "%d item left" + many = "%d items left" [filter] all = "All" diff --git a/example/app/pages/demos/todo/page.intl.sv-SE.toml b/example/app/pages/demos/todo/page.intl.sv-SE.toml index f4b8d2b7..e3dd2c49 100644 --- a/example/app/pages/demos/todo/page.intl.sv-SE.toml +++ b/example/app/pages/demos/todo/page.intl.sv-SE.toml @@ -2,8 +2,8 @@ what_needs_to_be_done = "Vad behöver göras?" clear_completed = "Rensa färdiga" [item] - one = "%d sak" - many = "%d saker" + one = "%d återstår" + many = "%d återstår" [filter] all = "Alla" diff --git a/example/app/pages/demos/tree/page.haml b/example/app/pages/demos/tree/page.haml index 56cdb12a..ae351684 100644 --- a/example/app/pages/demos/tree/page.haml +++ b/example/app/pages/demos/tree/page.haml @@ -38,8 +38,8 @@ - root = File.expand_path(".") -%article - %Heading(level=2) App tree +%article(lang=lang) + %Heading(level=2)= t(:title) %p = t(:description) diff --git a/example/app/pages/demos/tree/page.intl.en-US.toml b/example/app/pages/demos/tree/page.intl.en-US.toml index 169d39c3..b9249609 100644 --- a/example/app/pages/demos/tree/page.intl.en-US.toml +++ b/example/app/pages/demos/tree/page.intl.en-US.toml @@ -1 +1,2 @@ +title = "App tree" description = "This page shows the file structure of the example app." diff --git a/example/app/pages/demos/tree/page.intl.sv-SE.toml b/example/app/pages/demos/tree/page.intl.sv-SE.toml index 374e6af6..b3934e6d 100644 --- a/example/app/pages/demos/tree/page.intl.sv-SE.toml +++ b/example/app/pages/demos/tree/page.intl.sv-SE.toml @@ -1 +1,2 @@ +title = "Applikationsträd" description = "Denna sidan visar filstrukturen i exempelapplikationen." diff --git a/lib/mayu/component/base.rb b/lib/mayu/component/base.rb index cb5033d6..d6f085f1 100644 --- a/lib/mayu/component/base.rb +++ b/lib/mayu/component/base.rb @@ -61,8 +61,7 @@ def self.get_derived_state_from_props(props, state) sig { returns(String) } def lang - helpers.get_accepted_language(self.class.loaded_translations.keys) or - raise "There are no translations!" + props[:lang] or raise "There are no translations!" end sig { params(path: Symbol, replacements: T.untyped).returns(String) } @@ -74,9 +73,7 @@ def t(*path, **replacements) .fetch(lang) { return "No translations" } .dig(*path) - unless value - return "Missing translation for #{lang} at #{path.join(".")}!" - end + return "Missing translation for #{path.join(".")}!" unless value format(value, replacements) end diff --git a/lib/mayu/component/wrapper.rb b/lib/mayu/component/wrapper.rb index ac2a9e97..ea647591 100644 --- a/lib/mayu/component/wrapper.rb +++ b/lib/mayu/component/wrapper.rb @@ -53,6 +53,11 @@ def assets @instance.class.assets end + sig { returns(T::Array[String]) } + def available_languages + @instance.class.loaded_translations.keys + end + sig { returns(T.nilable(Resources::Resource)) } def resource if @instance.class.respond_to?(:__resource) diff --git a/lib/mayu/vdom/vnode.rb b/lib/mayu/vdom/vnode.rb index 420435a5..4f738fb2 100644 --- a/lib/mayu/vdom/vnode.rb +++ b/lib/mayu/vdom/vnode.rb @@ -17,7 +17,6 @@ class VNode sig { returns(Id) } attr_reader :id - sig { returns(Id) } attr_accessor :dom_parent_id @@ -28,18 +27,26 @@ def dom_id sig { returns(Descriptor) } attr_accessor :descriptor + sig { returns(Descriptor::ElementType) } def type = descriptor.type + sig { returns(Component::Props) } def props = descriptor.props + sig { returns(T.untyped) } def key = descriptor.key + sig { returns(Children) } attr_accessor :children sig { returns(T.nilable(Component::Wrapper)) } attr_reader :component + sig { returns(T.nilable(String)) } + def lang + end + sig { returns(T::Boolean) } def dom? = type.is_a?(Symbol) @@ -127,7 +134,15 @@ def fetch(url, method: :GET, headers: {}, body: nil) sig { returns(T.nilable(Component::Wrapper)) } def init_component - @component ||= Component.wrap(self, type, props) + type = self.type + return unless Component.component_class?(type) + type = T.cast(type, T.class_of(Component::Base)) + + @component ||= + begin + lang = @vtree.get_accepted_language(type.loaded_translations.keys) + Component.wrap(self, type, { **props, lang: }) + end end sig { params(path: String).void } diff --git a/lib/mayu/vdom/vtree.rb b/lib/mayu/vdom/vtree.rb index 54dcb64a..8054303a 100644 --- a/lib/mayu/vdom/vtree.rb +++ b/lib/mayu/vdom/vtree.rb @@ -350,7 +350,8 @@ def patch_vnode(ctx, vnode, descriptor, lifecycles:) if component.should_update?(descriptor.props, component.next_state) vnode.descriptor = descriptor prev_props, prev_state = component.props, component.state - component.props = descriptor.props + lang = get_accepted_language(component.available_languages) + component.props = { **descriptor.props, lang: } component.state = component.next_state.clone descriptors = add_comments_between_texts( diff --git a/mayu-live.gemspec b/mayu-live.gemspec index 0d23a4ff..2c3c7c09 100644 --- a/mayu-live.gemspec +++ b/mayu-live.gemspec @@ -63,4 +63,5 @@ Gem::Specification.new do |spec| spec.add_dependency "syntax_tree-xml", '~> 0.1.0' spec.add_dependency "terminal-table", "~> 3.0.1" spec.add_dependency "toml-rb", "~> 2.2.0" + spec.add_dependency "twitter_cldr", "~> 6.11.4" end From 669294e4fdc1e18306ffd1c9c7a49e1ddd477e42 Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Sat, 12 Nov 2022 09:24:47 -0500 Subject: [PATCH 10/15] Icons and random stuff --- example/app/components/Clock.haml | 29 ++++++++++++++++------- example/app/components/Form/Button.haml | 2 ++ example/app/components/Layout/Header.haml | 2 +- example/app/components/UI/Icon/fix.rb | 7 ++++++ example/app/components/UI/Image.haml | 2 +- example/app/pages/Counter.haml | 13 +++++----- example/app/pages/demos/layout.i18n.toml | 27 +++++++++++++++++++++ example/app/pages/demos/life/page.haml | 15 +++++------- example/app/root.haml | 7 +++--- example/app/root.intl.en-US.toml | 1 + example/app/root.intl.sv-SE.toml | 1 + 11 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 example/app/components/UI/Icon/fix.rb create mode 100644 example/app/pages/demos/layout.i18n.toml create mode 100644 example/app/root.intl.en-US.toml create mode 100644 example/app/root.intl.sv-SE.toml diff --git a/example/app/components/Clock.haml b/example/app/components/Clock.haml index 7dae1fec..325a83dc 100644 --- a/example/app/components/Clock.haml +++ b/example/app/components/Clock.haml @@ -1,8 +1,12 @@ :ruby + def self.get_initial_state(**) + time = Time.now.utc + 0.5 + { hour: time.hour, min: time.min, sec: time.sec } + end + def mount loop do - time = Time.now.utc - update(hour: time.hour, min: time.min, sec: time.sec) + update(self.class.get_initial_state) sleep 0.5 end end @@ -21,6 +25,10 @@ height: auto; } + .hand { + transition: transform 250ms ease-in-out; + } + :ruby stroke = props[:stroke] || "#000" fill = props[:fill] || "transparent" @@ -30,13 +38,16 @@ %svg.svg(width="100" height="100" viewBox="-100 -100 200 200" xmlns="http://www.w3.org/2000/svg") %circle(cx=0 cy=0 r=99 fill=fill stroke=stroke stroke-width="2") %g - - x1, y1 = [0, 0] - - x2, y2 = calculate_pos(state[:hour].to_f * 5, 50.0) - %line.hand(x1=x1 y1=y1 x2=x2 y2=y2 stroke=stroke stroke-width="6") - - x2, y2 = calculate_pos(state[:min].to_f, 60.0) - %line.hand(x1=x1 y1=y1 x2=x2 y2=y2 stroke=stroke stroke-width="3") - - x2, y2 = calculate_pos(state[:sec].to_f, 80.0) - %line.hand(x1=x1 y1=y1 x2=x2 y2=y2 stroke=stroke stroke-width="1") + - x1, y1, x2 = [0, 0, 0] + - y2 = -50 + - style = { transform: format("rotateZ(%.5fturn)", state[:hour] / 12.0) } + %line.hand(x1=x1 y1=y1 x2=x2 y2=y2 stroke=stroke stroke-width="6"){style:} + - y2 = -60 + - style = { transform: format("rotateZ(%.5fturn)", state[:hour] + state[:min] / 60.0) } + %line.hand(x1=x1 y1=y1 x2=x2 y2=y2 stroke=stroke stroke-width="3"){style:} + - y2 = -80 + - style = { transform: format("rotateZ(%.5fturn)", state[:hour] * 60.0 + state[:min] + state[:sec] / 60.0) } + %line.hand(x1=x1 y1=y1 x2=x2 y2=y2 stroke=stroke stroke-width="1"){style:} %circle(cx=0 cy=0 r=3 fill=stroke) %g = 12.times.map do |i| diff --git a/example/app/components/Form/Button.haml b/example/app/components/Form/Button.haml index 72bc031b..656ba24e 100644 --- a/example/app/components/Form/Button.haml +++ b/example/app/components/Form/Button.haml @@ -8,6 +8,7 @@ user-select: none; font-family: inherit; color: #fff; + --icon-color: #fff; } .button:not(:disabled):active { @@ -26,6 +27,7 @@ .button:disabled { background-color: #ccc; color: #666; + --icon-color: #666; cursor: not-allowed; } diff --git a/example/app/components/Layout/Header.haml b/example/app/components/Layout/Header.haml index f346c6fd..30d5a361 100644 --- a/example/app/components/Layout/Header.haml +++ b/example/app/components/Layout/Header.haml @@ -18,5 +18,5 @@ = t(:docs) %li %a.navLink(href="https://github.com/mayu-live/framework" target="_blank") - GitHub + = t(:github) %Icon.icon(icon="up-right-from-square") diff --git a/example/app/components/UI/Icon/fix.rb b/example/app/components/UI/Icon/fix.rb new file mode 100644 index 00000000..469b686c --- /dev/null +++ b/example/app/components/UI/Icon/fix.rb @@ -0,0 +1,7 @@ +puts "ICONS = {" + +Dir["*.svg"].each do |file| + puts " #{File.basename(file, ".svg").delete_suffix("-solid").inspect} => svg(#{file.inspect})," +end + +puts "}.transform_keys(&:to_s).freeze" diff --git a/example/app/components/UI/Image.haml b/example/app/components/UI/Image.haml index e946bea5..0752e459 100644 --- a/example/app/components/UI/Image.haml +++ b/example/app/components/UI/Image.haml @@ -25,7 +25,7 @@ classname = props[:class] -%img.img(style=inline_style loading=loading alt=alt class=classname){ +%img.img(style=inline_style loading=loading decoding="async" alt=alt class=classname){ src: image.src, sizes: image.sizes, srcset: image.srcset, diff --git a/example/app/pages/Counter.haml b/example/app/pages/Counter.haml index 11e5aaa3..8f817186 100644 --- a/example/app/pages/Counter.haml +++ b/example/app/pages/Counter.haml @@ -1,5 +1,6 @@ :ruby Card = import("/app/components/Layout/Card") + Icon = import("/app/components/UI/Icon") def self.get_initial_state(initial_value: 0, **) = { count: initial_value @@ -40,6 +41,9 @@ } .button { + display: flex; + align-items: center; + justify-content: center; flex: 1; border-radius: 100%; border: 0; @@ -53,14 +57,11 @@ --transition-length: 100ms; transition: filter var(--transition-length), - box-shadow var(--transition-length), - transform var(--transition-length); - transform: scale(1); + box-shadow var(--transition-length); } .button:hover { filter: brightness(110%); - transform: scale(1.2); } .button:active { @@ -77,7 +78,7 @@ .counter - decrement_disabled = state[:count].zero? %button.button(title="Decrement" onclick=handle_decrement disabled=decrement_disabled) - - + %Icon(name="minus" color="var(--bright)") %span.count= state[:count] %button.button(title="Increment" onclick=handle_increment) - + + %Icon(name="plus" color="var(--bright)") diff --git a/example/app/pages/demos/layout.i18n.toml b/example/app/pages/demos/layout.i18n.toml new file mode 100644 index 00000000..f2032b95 --- /dev/null +++ b/example/app/pages/demos/layout.i18n.toml @@ -0,0 +1,27 @@ +[en] +demos = "Demos" +i18n = "Translations" +pokemon = "Pokémon" +tree = "App tree" +form = "Form elements" +images = "Images" +svg = "Scalable Vector Graphics" +fuzzymatch = "Fuzzy matcher" +todo = "Todo app" +life = "Game of life" +events = "Events" +exceptions = "Exceptions" + +[sv] +demos = "Demos" +i18n = "Översättningar" +pokemon = "Pokémon" +tree = "Applikationsträd" +form = "Formulärelement" +images = "Bilder" +svg = "Scalable Vector Graphics" +fuzzymatch = "Fuzzymatchning" +todo = "Att göra-app" +life = "Livets spel (Game of life)" +events = "Händelser" +exceptions = "Exceptions" diff --git a/example/app/pages/demos/life/page.haml b/example/app/pages/demos/life/page.haml index 42379723..692849ff 100644 --- a/example/app/pages/demos/life/page.haml +++ b/example/app/pages/demos/life/page.haml @@ -143,10 +143,7 @@ .button { display: flex; align-items: center; - } - - .icon { - margin-right: .5em; + gap: .5em; } :ruby @@ -159,20 +156,20 @@ %legend= t(:controls) .buttons %Button.button(onclick=handle_reset) - %Icon.icon(name="refresh") + %Icon(name="arrows-rotate") = t(:reset) %Button.button(onclick=handle_randomize) - %Icon.icon(name="dice") + %Icon(name="dice") = t(:randomize) %Button.button(onclick=handle_step disabled=running) - %Icon.icon(name="forward_step") + %Icon(name="forward-step") = t(:step) = if running %Button.button(onclick=handle_toggle_running color="var(--red)") - %Icon.icon(name="pause") + %Icon(name="pause") = t(:stop) = unless running %Button.button(onclick=handle_toggle_running color="var(--green)") - %Icon.icon(name="play") + %Icon(name="play") = t(:start) %GameGrid(grid=grid size=size onmousedown=handle_toggle onmouseenter=handle_mouseenter) diff --git a/example/app/root.haml b/example/app/root.haml index b8f2c5b4..5f97dd34 100644 --- a/example/app/root.haml +++ b/example/app/root.haml @@ -1,9 +1,10 @@ -%html +:ruby + translations("sv-SE", "en-US") +%html(lang="en") %head %meta(name="charset" value="utf-8") %meta(name="generator" value="github.com/mayu-live/framework") %meta(name="viewport" content="width=device-width, initial-scale=1") - %title - Mayu Live + %title= t(:title) %body.body %slot diff --git a/example/app/root.intl.en-US.toml b/example/app/root.intl.en-US.toml new file mode 100644 index 00000000..0831e656 --- /dev/null +++ b/example/app/root.intl.en-US.toml @@ -0,0 +1 @@ +title = "Mayu Live" diff --git a/example/app/root.intl.sv-SE.toml b/example/app/root.intl.sv-SE.toml new file mode 100644 index 00000000..0831e656 --- /dev/null +++ b/example/app/root.intl.sv-SE.toml @@ -0,0 +1 @@ +title = "Mayu Live" From c2999c022ddeeee7a25f350bddfccf99f64726cc Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Sun, 13 Nov 2022 10:51:38 -0500 Subject: [PATCH 11/15] crass patches --- .../transformers/css/crass_patches.rb | 55 +++++++++++++++++++ .../resources/transformers/css/transformer.rb | 1 + 2 files changed, 56 insertions(+) create mode 100644 lib/mayu/resources/transformers/css/crass_patches.rb diff --git a/lib/mayu/resources/transformers/css/crass_patches.rb b/lib/mayu/resources/transformers/css/crass_patches.rb new file mode 100644 index 00000000..a8338376 --- /dev/null +++ b/lib/mayu/resources/transformers/css/crass_patches.rb @@ -0,0 +1,55 @@ +# typed: false +require "crass" + +# This patch parses @media and @layer as style rules. + +module Crass + class Parser + def consume_at_rule(input = @tokens) + rule = {} + + rule[:tokens] = input.collect do + rule[:name] = input.consume[:value] + rule[:prelude] = [] + + while token = input.consume + node = token[:node] + + if node == :comment + # Non-standard. + next + elsif node == :semicolon + break + elsif node === :"{" + case rule[:name] + in "media" | "layer" + rule[:rules] = consume_rules.map do |rule| + rule[:node] == :qualified_rule ? create_style_rule(rule) : rule + end + else + # Note: The spec says the block should _be_ the consumed simple + # block, but Simon Sapin's CSS parsing tests and tinycss2 expect + # only the _value_ of the consumed simple block here. I assume I'm + # interpreting the spec too literally, so I'm going with the + # tinycss2 behavior. + rule[:block] = consume_simple_block(input)[:value] + end + break + elsif node == :simple_block && token[:start] == "{" + # Note: The spec says the block should _be_ the simple block, but + # Simon Sapin's CSS parsing tests and tinycss2 expect only the + # _value_ of the simple block here. I assume I'm interpreting the + # spec too literally, so I'm going with the tinycss2 behavior. + rule[:block] = token[:value] + break + else + input.reconsume + rule[:prelude] << consume_component_value(input) + end + end + end + + create_node(:at_rule, rule) + end + end +end diff --git a/lib/mayu/resources/transformers/css/transformer.rb b/lib/mayu/resources/transformers/css/transformer.rb index 825b5a1a..bd7de712 100644 --- a/lib/mayu/resources/transformers/css/transformer.rb +++ b/lib/mayu/resources/transformers/css/transformer.rb @@ -5,6 +5,7 @@ require "base64" require "digest/sha2" require "set" +require_relative "crass_patches" module Mayu module Resources From e010cbcae84b29672b55c53fdcf4fd7220a4b0d6 Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Sun, 13 Nov 2022 10:52:10 -0500 Subject: [PATCH 12/15] accept language defaults --- lib/mayu/session.rb | 12 +++++++----- lib/mayu/vdom/vtree.rb | 6 +++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/mayu/session.rb b/lib/mayu/session.rb index 9f50d860..35653339 100644 --- a/lib/mayu/session.rb +++ b/lib/mayu/session.rb @@ -171,13 +171,15 @@ def accept_language @accept_language ||= begin accept_language = - AcceptLanguage.parse(@headers["accept-language"].to_s) + AcceptLanguage.parse(Array(@headers["accept-language"]).join(",")) if @prefer_language - accept_language.instance_variable_get(:@languages_range).store( - @prefer_language, - BigDecimal(2) - ) + accept_language + .instance_variable_get(:@languages_range) + .tap do |languages_range| + languages_range.store(@prefer_language, BigDecimal(2)) + languages_range["en-US"] ||= BigDecimal("0.01") + end end accept_language diff --git a/lib/mayu/vdom/vtree.rb b/lib/mayu/vdom/vtree.rb index 8054303a..56ff9b05 100644 --- a/lib/mayu/vdom/vtree.rb +++ b/lib/mayu/vdom/vtree.rb @@ -187,9 +187,13 @@ def initialize(session:, task: Async::Task.current) @asset_refs = T.let(RefCounter.new, RefCounter[String]) end + DEFAULT_ACCEPT_LANGUAGE = + T.let(AcceptLanguage.parse("en, *;q=0.5"), AcceptLanguage::Parser) + sig { params(languages: T::Array[String]).returns(T.nilable(String)) } def get_accepted_language(languages) - T.unsafe(@session.accept_language).match(*languages) + T.unsafe(@session.accept_language).match(*languages) || + T.unsafe(DEFAULT_ACCEPT_LANGUAGE).match(*languages) || languages.first end sig { params(language: String).void } From 84ef199c60d3d0d3e44316050a72d7e7441be3be Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Sun, 13 Nov 2022 10:53:16 -0500 Subject: [PATCH 13/15] add json lexer --- example/app/components/Layout/Highlight.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/app/components/Layout/Highlight.haml b/example/app/components/Layout/Highlight.haml index a432d52e..4d15f665 100644 --- a/example/app/components/Layout/Highlight.haml +++ b/example/app/components/Layout/Highlight.haml @@ -20,6 +20,8 @@ Rouge::Lexers::HTML.new when :css Rouge::Lexers::CSS.new + when :json + Rouge::Lexers::JSON.new end end From ba4f11b653bfa769b7c410210e258c23706d1bcb Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Sun, 13 Nov 2022 10:53:33 -0500 Subject: [PATCH 14/15] use languages icon --- example/app/components/Layout/Languages.haml | 4 +++- example/app/components/Layout/Languages.intl.en-US.toml | 2 +- example/app/components/Layout/Languages.intl.sv-SE.toml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/example/app/components/Layout/Languages.haml b/example/app/components/Layout/Languages.haml index 9297c4f0..8db2bc0f 100644 --- a/example/app/components/Layout/Languages.haml +++ b/example/app/components/Layout/Languages.haml @@ -1,6 +1,8 @@ :ruby translations("en-US", "sv-SE") + Icon = import("/app/components/UI/Icon") + def handle_set_language(e) e => { target: { value: } } helpers.set_prefer_language(value) @@ -30,6 +32,6 @@ text-decoration: underline; } %section.languages(lang=lang) - %h3.title= t(:title) + %Icon(name="language"){title: t(:title), style: { aspect_ratio: "1.25", width: "2em" }} %button.button(onclick=handle_set_language value="sv-SE" lang="sv-SE") Svenska %button.button(onclick=handle_set_language value="en-US" lang="en-US") English (US) diff --git a/example/app/components/Layout/Languages.intl.en-US.toml b/example/app/components/Layout/Languages.intl.en-US.toml index 32888440..fffdfad8 100644 --- a/example/app/components/Layout/Languages.intl.en-US.toml +++ b/example/app/components/Layout/Languages.intl.en-US.toml @@ -1 +1 @@ -title = "Change language:" +title = "Change language" diff --git a/example/app/components/Layout/Languages.intl.sv-SE.toml b/example/app/components/Layout/Languages.intl.sv-SE.toml index be3e8368..7fdddea0 100644 --- a/example/app/components/Layout/Languages.intl.sv-SE.toml +++ b/example/app/components/Layout/Languages.intl.sv-SE.toml @@ -1 +1 @@ -title = "Byt språk:" +title = "Byt språk" From 7d82f7240fd0b37ac205f2db9c3d737ad6e338ce Mon Sep 17 00:00:00 2001 From: Andreas Alin Date: Sun, 13 Nov 2022 11:37:12 -0500 Subject: [PATCH 15/15] asd --- example/app/components/Layout/Header.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/app/components/Layout/Header.haml b/example/app/components/Layout/Header.haml index 30d5a361..4ab7f369 100644 --- a/example/app/components/Layout/Header.haml +++ b/example/app/components/Layout/Header.haml @@ -19,4 +19,4 @@ %li %a.navLink(href="https://github.com/mayu-live/framework" target="_blank") = t(:github) - %Icon.icon(icon="up-right-from-square") + %Icon.icon(name="up-right-from-square")